Vlastný bezpečnostný výraz s jarným zabezpečením
1. Prehľad
V tejto príručke sa zameriame na vytvorenie vlastného bezpečnostného výrazu pomocou Spring Security.
Niekedy nie sú výrazy dostupné v rámci rámca dostatočne expresívne. A v týchto prípadoch je pomerne jednoduché vytvoriť nový výraz, ktorý je sémanticky bohatší ako tie súčasné.
Najprv si povieme, ako vytvoriť zvyk Hodnotiteľ povolení, potom úplne prispôsobený výraz - a nakoniec, ako prepísať jeden zo zabudovaných bezpečnostných výrazov.
2. Subjekt používateľa
Najskôr si pripravme základ pre vytváranie nových bezpečnostných výrazov.
Pozrime sa na našu Používateľ subjekt - ktorý má a Výsady a an Organizácia:
@Entity verejná trieda Používateľ {@Id @GeneratedValue (strategy = GenerationType.AUTO) private Long id; @Column (nullable = false, unique = true) private String username; súkromné reťazcové heslo; @ManyToMany (fetch = FetchType.EAGER) @JoinTable (name = "users_privileges", joinColumns = @JoinColumn (name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn (name = "privilege_id") id ")) privátne nastavenia privilégií; @ManyToOne (fetch = FetchType.EAGER) @JoinColumn (name = "organization_id", referencedColumnName = "id") súkromná organizácia organizácie; // štandardné getre a setre}
A tu je náš jednoduchý Výsada:
@Entity public class Privilege {@Id @GeneratedValue (strategy = GenerationType.AUTO) private Long id; @Column (nullable = false, unique = true) private String name; // štandardné getre a setre}
A náš Organizácia:
@Entity public class Organisation {@Id @GeneratedValue (strategy = GenerationType.AUTO) private Long id; @Column (nullable = false, unique = true) private String name; // štandardní zakladatelia a obstarávatelia}
Na záver - použijeme jednoduchší zvyk Principal:
verejná trieda MyUserPrincipal implementuje UserDetails {súkromný používateľ; public MyUserPrincipal (užívateľský užívateľ) {this.user = užívateľ; } @Override public String getUsername () {return user.getUsername (); } @Override public String getPassword () {return user.getPassword (); } @ Verzia Verejná zbierka getAuthorities () {Zoznam autorít = nový ArrayList (); pre (Privilege privilege: user.getPrivileges ()) {Authority.add (new SimpleGrantedAuthority (privilege.getName ())); } návratové orgány; } ...}
Keď budú všetky tieto triedy pripravené, použijeme svoj zvyk Principal v zákl UserDetailsService implementácia:
@ Verejná trieda služby MyUserDetailsService implementuje UserDetailsService {@Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername (String username) {User user = userRepository.findByUsername (username); if (user == null) {throw new UsernameNotFoundException (username); } vrátiť nový MyUserPrincipal (užívateľ); }}
Ako vidíte, na týchto vzťahoch nie je nič zložité - používateľ má jedno alebo viac privilégií a každý používateľ patrí do jednej organizácie.
3. Nastavenie údajov
Ďalej - inicializujme našu databázu jednoduchými testovacími údajmi:
@ Komponenta verejná trieda SetupData {@Autowired private UserRepository userRepository; @Autowired private PrivilegeRepository privilegeRepository; @Autowired private OrganizationRepository organizationRepository; @PostConstruct public void init () {initPrivileges (); initOrganizations (); initUsers (); }}
Tu je náš init metódy:
private void initPrivileges () {Privilege privilege1 = new Privilege ("FOO_READ_PRIVILEGE"); privilegeRepository.save (privilégium1); Privilege privilege2 = new Privilege ("FOO_WRITE_PRIVILEGE"); privilegeRepository.save (privilégium2); }
private void initOrganizations () {Organizácia org1 = nová organizácia ("FirstOrg"); organizationRepository.save (org1); Organizácia org2 = nová organizácia („SecondOrg“); organizationRepository.save (org2); }
private void initUsers () {Privilege privilege1 = privilegeRepository.findByName ("FOO_READ_PRIVILEGE"); Privilege privilege2 = privilegeRepository.findByName ("FOO_WRITE_PRIVILEGE"); Používateľ user1 = nový Používateľ (); user1.setUsername ("john"); user1.setPassword ("123"); user1.setPrivileges (new HashSet (Arrays.asList (privilege1))); user1.setOrganization (organizationRepository.findByName ("FirstOrg")); userRepository.save (user1); Používateľ user2 = nový používateľ (); user2.setUsername ("tom"); user2.setPassword ("111"); user2.setPrivileges (new HashSet (Arrays.asList (privilege1, privilege2))); user2.setOrganization (organizationRepository.findByName ("SecondOrg")); userRepository.save (user2); }
Poznač si to:
- Používateľ „john“ má iba FOO_READ_PRIVILEGE
- Používateľ „tom“ má oboje FOO_READ_PRIVILEGE a FOO_WRITE_PRIVILEGE
4. Hodnotiteľ vlastného povolenia
V tomto okamihu sme pripravení začať implementovať náš nový výraz - prostredníctvom nového, vlastného hodnotiča povolení.
Na zabezpečenie našich metód použijeme oprávnenie používateľa. Namiesto používania pevne zakódovaných názvov privilégií však chceme dosiahnuť otvorenejšiu a flexibilnejšiu implementáciu.
Začnime.
4.1. Hodnotiteľ povolení
Na to, aby sme si mohli vytvoriť vlastného hodnotiteľa povolení, musíme implementovať Hodnotiteľ povolení rozhranie:
verejná trieda CustomPermissionEvaluator implementuje PermissionEvaluator {@Override public boolean hasPermission (Authentication auth, Object targetDomainObject, Object permission) {if ((auth == null) || (targetDomainObject == null) ||! (permission instanceof String)) {return false ; } Reťazec targetType = targetDomainObject.getClass (). GetSimpleName (). ToUpperCase (); návrat hasPrivilege (auth, targetType, permission.toString (). toUpperCase ()); } @Override public boolean hasPermission (Authentication auth, Serializable targetId, String targetType, Object permission) {if ((auth == null) || (targetType == null) ||! (Permission instanceof String)) {return false; } návrat hasPrivilege (auth, targetType.toUpperCase (), permission.toString (). toUpperCase ()); }}
Tu je náš hasPrivilege () metóda:
private boolean hasPrivilege (Authentication auth, String targetType, String permission) {for (GrantedAuthority GrantedAuth: auth.getAuthorities ()) {if (GrantedAuth.getAuthority (). StartWith (TargetType)) {if (GrantedAuth.getAuthority (). povolenie)) {návrat pravdivý; }}} return false; }
Teraz máme k dispozícii nový bezpečnostný výraz pripravený na použitie: máPovolenie.
A tak namiesto použitia programovanejšej verzie:
@PostAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')")
Môžeme použiť:
@PostAuthorize ("hasPermission (returnObject, 'read')")
alebo
@PreAuthorize ("hasPermission (#id, 'Foo', 'read')")
Poznámka: #id odkazuje na parameter metódy a „Foo‘Odkazuje na typ cieľového objektu.
4.2. Konfigurácia zabezpečenia metódy
Nestačí definovať CustomPermissionEvaluator - musíme to tiež použiť v našej konfigurácii zabezpečenia metódy:
@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) verejná trieda MethodSecurityConfig rozširuje GlobalMethodSecurityConfiguration {@Override chránený MethodSecurityExpressionHandler createExpressionHandler () {DefaultMethodSecurityExpressionHandler expressionHandler = nový DefaultMethodSeturityConfig expressionHandler.setPermissionEvaluator (nový CustomPermissionEvaluator ()); návrat výrazHandler; }}
4.3. Príklad v praxi
Začnime teraz využívať nový výraz - niekoľkými jednoduchými metódami radiča:
@Controller public class MainController {@PostAuthorize ("hasPermission (returnObject, 'read')") @GetMapping ("/ foos / {id}") @ResponseBody public Foo findById (@PathVariable long id) {return new Foo ("Sample "); } @PreAuthorize ("hasPermission (#foo, 'write')") @PostMapping ("/ foos") @ResponseStatus (HttpStatus.CREATED) @ResponseBody public Foo create (@RequestBody Foo foo) {return foo; }}
A sme tu - všetci sme pripravení a nový výraz využívame v praxi. Poďme teraz napísať jednoduché živé testy - zásah do API a uistenie sa, že je všetko v poriadku: A tu je náš danéAuth () metóda: S predchádzajúcim riešením sme boli schopní definovať a používať máPovolenie výraz - čo môže byť celkom užitočné. Stále nás tu však niečo obmedzuje názov a sémantika samotného výrazu. A tak v tejto časti pôjdeme úplne na mieru - a implementujeme bezpečnostný výraz s názvom isMember () - kontrola, či je príkazca členom organizácie. Aby sme mohli vytvoriť tento nový vlastný výraz, musíme začať implementáciou koreňovej poznámky, kde začína hodnotenie všetkých bezpečnostných výrazov: Teraz, ako sme poskytli túto novú operáciu priamo v koreňovej poznámke tu; isMember () slúži na kontrolu, či je aktuálny užívateľ členom Organizácia. Všimnite si tiež, ako sme predĺžili SecurityExpressionRoot zahrnúť aj zabudované výrazy. Ďalej si musíme vpichnúť náš CustomMethodSecurityExpressionRoot v našom ovládači výrazov: Teraz musíme použiť naše CustomMethodSecurityExpressionHandler v konfigurácii zabezpečenia metódy: Tu je jednoduchý príklad na zabezpečenie našej metódy radiča pomocou isMember (): Na záver je tu jednoduchý test používateľa „john“: Na záver sa pozrime, ako prepísať zabudovaný bezpečnostný výraz - diskutujeme o deaktivácii hasAuthority (). Začneme podobne tým, že napíšeme svoje vlastné SecurityExpressionRoot - hlavne preto, že zabudované metódy sú konečné a preto ich nemôžeme prepísať: Po definovaní tejto koreňovej poznámky ju budeme musieť vložiť do obslužnej rutiny výrazov a potom ju zapojiť do našej konfigurácie - rovnako ako sme to urobili vyššie v časti 5. Teraz, ak chceme použiť hasAuthority () na zabezpečenie metód - takto bude vrhať RuntimeException keď sa pokúšame získať prístup k metóde: Na záver je tu náš jednoduchý test: V tejto príručke sme sa hlboko ponorili do rôznych spôsobov, ako môžeme implementovať vlastný bezpečnostný výraz v Spring Security, ak tie súčasné nestačia. Celý zdrojový kód nájdete ako vždy na serveri GitHub.4.4. Test naživo
@Test public void givenUserWithReadPrivilegeAndHasPermission_whenGetFooById_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos / 1"); assertEquals (200, response.getStatusCode ()); assertTrue (response.asString (). contains ("id")); } @Test public void givenUserWithNoWritePrivilegeAndHasPermission_whenPostFoo_thenForbidden () {Response response = givenAuth ("john", "123"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (new Foo ("sample") foos "); assertEquals (403, response.getStatusCode ()); } @Test public void givenUserWithWritePrivilegeAndHasPermission_whenPostFoo_thenOk () {Response response = givenAuth ("tom", "111"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (new Foo ("sample")) .post ("//" foos "); assertEquals (201, response.getStatusCode ()); assertTrue (response.asString (). contains ("id")); }
private RequestSpecification givenAuth (meno používateľa reťazca, heslo reťazca) {FormAuthConfig formAuthConfig = nový FormAuthConfig ("// localhost: 8082 / login", "meno používateľa", "heslo"); vrátiť RestAssured.given (). auth (). form (používateľské meno, heslo, formAuthConfig); }
5. Nový bezpečnostný výraz
5.1. Bezpečnostný výraz vlastnej metódy
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {public CustomMethodSecurityExpressionRoot (Authentication authentication) {super (authentication); } public boolean isMember (Long OrganizationId) {User user = ((MyUserPrincipal) this.getPrincipal ()). getUser (); návrat user.getOrganization (). getId (). longValue () == OrganizationId.longValue (); } ...}
5.2. Obsluha vlastných výrazov
verejná trieda CustomMethodSecurityExpressionHandler rozširuje DefaultMethodSecurityExpressionHandler {private AuthenticationTrustResolver trustResolver = nový AuthenticationTrustResolverImpl (); @Override chránený MethodSecurityExpressionOperations createSecurityExpressionRoot (autentifikácia autentifikácia, vyvolanie MethodInvocation) {CustomMethodSecurityExpressionRoot root = nový CustomMethodSecurityExpressionRoot (autentifikácia); root.setPermissionEvaluator (getPermissionEvaluator ()); root.setTrustResolver (this.trustResolver); root.setRoleHierarchy (getRoleHierarchy ()); návratový koreň; }}
5.3. Konfigurácia zabezpečenia metódy
@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) verejná trieda MethodSecurityConfig rozširuje GlobalMethodSecurityConfiguration {@Override chránený MethodSecurityExpressionHandler createExpressionHandler () {CustomMethodSecurityExpressionHandler expressionHandler = nový CustomMethodHandler = nový expressionHandler.setPermissionEvaluator (nový CustomPermissionEvaluator ()); návrat výrazHandler; }}
5.4. Používanie nového výrazu
@PreAuthorize ("isMember (#id)") @GetMapping ("/ organization / {id}") @ResponseBody public Organization findOrgById (@PathVariable long id) {return organizationRepository.findOne (id); }
5.5. Živý test
@Test public void givenUserMemberInOrganization_whenGetOrganization_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organization / 1"); assertEquals (200, response.getStatusCode ()); assertTrue (response.asString (). contains ("id")); } @Test public void givenUserMemberNotInOrganization_whenGetOrganization_thenForbidden () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organization / 2"); assertEquals (403, response.getStatusCode ()); }
6. Zakážte vstavaný bezpečnostný výraz
6.1. Root vlastného bezpečnostného výrazu
verejná trieda MySecurityExpressionRoot implementuje MethodSecurityExpressionOperations {public MySecurityExpressionRoot (autentifikácia autentifikácie) {if (autentifikácia == null) {hodiť novú IllegalArgumentException ("autentifikačný objekt nemôže byť null"); } this.authentication = autentifikácia; } @Override public final boolean hasAuthority (reťazcový orgán) {hodiť novú RuntimeException ("metóda hasAuthority () nie je povolená"); } ...}
6.2. Príklad - použitie výrazu
@PreAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')") @GetMapping ("/ foos") @ResponseBody public Foo findFooByName (@RequestParam názov reťazca) {return new Foo (name); }
6.3. Živý test
@Test public void givenDisabledSecurityExpression_whenGetFooByName_thenError () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos? Name = sample"); assertEquals (500, response.getStatusCode ()); assertTrue (response.asString (). contains ("metóda hasAuthority () nie je povolená")); }
7. Záver