Prilagođeni sigurnosni izraz s proljetnom sigurnošću
1. Pregled
U ovom uputstvu usredotočit ćemo se na stvaranje prilagođenog sigurnosnog izraza s Spring Security.
Ponekad izrazi dostupni u okviru jednostavno nisu dovoljno izražajni. U tim je slučajevima relativno jednostavno stvoriti novi izraz koji je semantički bogatiji od postojećeg.
Prvo ćemo razgovarati o tome kako stvoriti običaj Procjenitelj dozvole, zatim potpuno prilagođeni izraz - i na kraju kako nadjačati jedan od ugrađenih sigurnosnih izraza.
2. Korisnički entitet
Prvo, pripremimo temelj za stvaranje novih sigurnosnih izraza.
Pogledajmo naše Korisnik entitet - koji ima Privilegije i an Organizacija:
@Entity javni razred Korisnik {@Id @GeneratedValue (strategy = GenerationType.AUTO) private Long id; @Column (nullable = false, unique = true) privatno korisničko ime niza; privatna lozinka za niz; @ManyToMany (fetch = FetchType.EAGER) @JoinTable (name = "user_privileges", joinColumns = @JoinColumn (name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn (name = "privilege_idN, reference =" privilege_idN ", id ")) private Set privilegija; @ManyToOne (fetch = FetchType.EAGER) @JoinColumn (name = "organization_id", referencedColumnName = "id") privatna organizacija organizacije; // standardni geteri i postavljači}
I ovdje je naše jednostavno Privilegija:
@Entity javna klasa Privilege {@Id @GeneratedValue (strategija = GenerationType.AUTO) private Long id; @Column (nullable = false, unique = true) ime privatnog niza; // standardni geteri i postavljači}
I naše Organizacija:
@ Entiteta javna klasa Organizacija {@Id @GeneratedValue (strategija = GenerationType.AUTO) private Long id; @Column (nullable = false, unique = true) ime privatnog niza; // standardni postavljači i dobivači}
Napokon - poslužit ćemo se jednostavnijim običajem Glavni:
javna klasa MyUserPrincipal implementira UserDetails {privatni korisnik; javni MyUserPrincipal (Korisnik korisnik) {this.user = korisnik; } @Override javni niz getUsername () {return user.getUsername (); } @Override javni niz getPassword () {return user.getPassword (); } @Override javna zbirka getAuthorities () {Ovlasti popisa = novi ArrayList (); za (Privilege privilegija: user.getPrivileges ()) {organi vlasti.add (novi SimpleGrantedAuthority (privilege.getName ())); } vlasti za povratak; } ...}
Kada su svi ovi tečajevi spremni, poslužit ćemo se našim običajima Glavni u osnovnom UserDetailsService provedba:
@Service javna klasa MyUserDetailsService implementira UserDetailsService {@Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername (String username) {User user = userRepository.findByUsername (username); if (user == null) {baciti novo UsernameNotFoundException (korisničko ime); } vratiti novog MyUserPrincipal (korisnik); }}
Kao što vidite, u tim odnosima nema ništa komplicirano - korisnik ima jednu ili više privilegija, a svaki korisnik pripada jednoj organizaciji.
3. Postavljanje podataka
Dalje - inicijalizirajmo našu bazu podataka jednostavnim testnim podacima:
@Component javna klasa SetupData {@Autowired private UserRepository userRepository; @Autowired privatni PrivilegeRepository privilegeRepository; @Autowired privatni OrganizationRepository organizationRepository; @PostConstruct javna void init () {initPrivileges (); initOrganizations (); initUsers (); }}
Ovdje je naš u tome metode:
private void initPrivileges () {Privilege privilege1 = novi Privilege ("FOO_READ_PRIVILEGE"); privilegeRepository.save (privilege1); Povlastica privilegija2 = nova privilegija ("FOO_WRITE_PRIVILEGE"); privilegeRepository.save (privilege2); }
private void initOrganizations () {Organizacija org1 = nova Organizacija ("FirstOrg"); organizationRepository.save (org1); Org2 organizacije = nova organizacija ("SecondOrg"); organizationRepository.save (org2); }
private void initUsers () {Privilege privilege1 = privilegeRepository.findByName ("FOO_READ_PRIVILEGE"); Privilegij privilege2 = privilegeRepository.findByName ("FOO_WRITE_PRIVILEGE"); Korisnik user1 = novi korisnik (); user1.setUsername ("john"); user1.setPassword ("123"); user1.setPrivileges (novi HashSet (Arrays.asList (privilege1))); user1.setOrganization (organizationRepository.findByName ("FirstOrg")); userRepository.save (user1); Korisnik user2 = novi korisnik (); user2.setUsername ("tom"); user2.setPassword ("111"); user2.setPrivileges (novi HashSet (Arrays.asList (privilege1, privilege2))); user2.setOrganization (organizationRepository.findByName ("SecondOrg")); userRepository.save (user2); }
Imajte na umu da:
- Korisnik "john" ima samo FOO_READ_PRIVILEGE
- Korisnik "tom" ima oboje FOO_READ_PRIVILEGE i FOO_WRITE_PRIVILEGE
4. Prilagođeni procjenitelj dozvola
U ovom smo trenutku spremni započeti s implementacijom našeg novog izraza - putem novog, prilagođenog ocjenjivača dopuštenja.
Upotrijebit ćemo korisničke privilegije da osiguramo naše metode - ali umjesto da koristimo tvrdo kodirana imena privilegija, želimo doći do otvorenije, fleksibilnije implementacije.
Započnimo.
4.1. Procjenitelj dozvole
Da bismo stvorili vlastiti ocjenjivač prilagođenih dozvola, moramo implementirati Procjenitelj dozvole sučelje:
javna klasa CustomPermissionEvaluator implementira PermissionEvaluator {@Override public boolean hasPermission (Authentication auth, Object targetDomainObject, Object dozvola) {if ((auth == null) || (targetDomainObject == null) ||! (instance instance of String)) {return false ; } Niz targetType = targetDomainObject.getClass (). GetSimpleName (). ToUpperCase (); vrati hasPrivilege (auth, targetType, dozvoла.toString (). toUpperCase ()); } @Override public boolean hasPermission (Authentication auth, Serializable targetId, String targetType, Object dozvola) {if ((auth == null) || (targetType == null) ||! (Instance instanceof String)) {return false; } return hasPrivilege (auth, targetType.toUpperCase (), dozvola.toString (). toUpperCase ()); }}
Ovdje je naš hasPrivilege () metoda:
private boolean hasPrivilege (Authentication auth, String targetType, String dozvola) {for (GrantedAuthority grantAuth: auth.getAuthorities ()) {if (grantAuth.getAuthority (). startWith (targetType)) {if (grantAuth.getAuthority (). sadrži ( dopuštenje)) {return true; }}} return false; }
Sada imamo novi sigurnosni izraz dostupan i spreman za upotrebu: imaDopuštenje.
I tako, umjesto korištenja tvrđe kodirane verzije:
@PostAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')")
Možemo koristiti korištenje:
@PostAuthorize ("hasPermission (returnObject, 'read')")
ili
@PreAuthorize ("hasPermission (#id, 'Foo', 'read')")
Bilješka: #iskaznica odnosi se na parametar metode i 'Foo'Odnosi se na ciljani tip objekta.
4.2. Konfiguracija sigurnosti metode
Nije dovoljno definirati CustomPermissionEvaluator - također ga trebamo koristiti u našoj sigurnosnoj konfiguraciji metode:
@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) javna klasa MethodSecurityConfig proširuje GlobalMethodSecurityConfiguration {@Override protected MethodSecurityExpressionHandler createExpressionHandler () {DefaultMethodSecurityExpressionHandler expressionHandler; expressionHandler.setPermissionEvaluator (novi CustomPermissionEvaluator ()); return expressionHandler; }}
4.3. Primjer u praksi
Počnimo sada koristiti novi izraz - u nekoliko jednostavnih metoda kontrolera:
@Controller javna klasa 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; }}
I tu smo - svi smo spremni i koristimo novi izraz u praksi. Napišimo sada jednostavne testove uživo - pogađanje API-ja i provjeravanje je li sve u redu: I ovdje je naš givenAuth () metoda: S prethodnim rješenjem uspjeli smo definirati i koristiti imaDopuštenje izraz - što može biti vrlo korisno. Međutim, ovdje smo još uvijek donekle ograničeni imenom i semantikom samog izraza. I tako, u ovom ćemo odjeljku ići u potpunosti po mjeri - i implementirat ćemo sigurnosni izraz tzv isMember () - provjera je li nalogodavac član organizacije. Da bismo stvorili ovaj novi prilagođeni izraz, trebamo započeti s implementacijom matične bilješke gdje započinje procjena svih sigurnosnih izraza: Sad, kako smo pružili ovu novu operaciju upravo u korijenskoj bilješci ovdje; isMember () koristi se za provjeru je li trenutni korisnik član u danom Organizacija. Također imajte na umu kako smo produžili SecurityExpressionRoot uključiti i ugrađene izraze. Dalje, trebamo ubrizgati svoje CustomMethodSecurityExpressionRoot u našem obrađivaču izraza: Sada moramo koristiti svoje CustomMethodSecurityExpressionHandler u konfiguraciji zaštite metode: Evo jednostavnog primjera za zaštitu naše metode kontrolera isMember (): Na kraju, evo jednostavnog testa za korisnika za korisnika “Ivan“: Na kraju, pogledajmo kako nadjačati ugrađeni sigurnosni izraz - razgovarat ćemo o onemogućavanju hasAuthority (). Započet ćemo slično pisanjem vlastitog SecurityExpressionRoot - uglavnom zato što su ugrađene metode konačni i zato ih ne možemo nadjačati: Nakon definiranja ove korijenske bilješke, morat ćemo je ubrizgati u obrađivač izraza, a zatim spojiti taj obrađivač u našu konfiguraciju - baš kao što smo to učinili gore u odjeljku 5. Sada, ako želimo koristiti hasAuthority () osigurati metode - kako slijedi, bacit će RuntimeException kada pokušavamo pristupiti metodi: Na kraju, evo našeg jednostavnog testa: U ovom smo vodiču detaljno zarobili razne načine na koje možemo implementirati prilagođeni sigurnosni izraz u Spring Security, ako postojeći nisu dovoljni. Kao i uvijek, puni izvorni kod možete pronaći na GitHubu.4.4. Test uživo
@Test javna praznina givenUserWithReadPrivilegeAndHasPermission_whenGetFooById_thenOK () {Odgovor odgovora = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos / 1"); assertEquals (200, response.getStatusCode ()); assertTrue (response.asString (). sadrži ("id")); } @Test javna praznina givenUserWithNoWritePrivilegeAndHasPermission_whenPostFoo_thenForbidden () {Odgovor odgovora = givenAuth ("john", "123"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (new Foo ("sample") )st. foos "); assertEquals (403, response.getStatusCode ()); } @Test javna praznina givenUserWithWritePrivilegeAndHasPermission_whenPostFoo_thenOk () {Odgovor odgovora = givenAuth ("tom", "111"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (new Foo ("sample")) .post ("// localhost foos "); assertEquals (201, response.getStatusCode ()); assertTrue (response.asString (). sadrži ("id")); }
private RequestSpecification givenAuth (korisničko ime niza, lozinka niza) {FormAuthConfig formAuthConfig = novi FormAuthConfig ("// localhost: 8082 / login", "username", "password"); vratiti RestAssured.given (). auth (). form (korisničko ime, lozinka, formAuthConfig); }
5. Novi sigurnosni izraz
5.1. Izražavanje sigurnosti prilagođene metode
javna klasa CustomMethodSecurityExpressionRoot proširuje SecurityExpressionRoot implementira MethodSecurityExpressionOperations {public CustomMethodSecurityExpressionRoot (provjera autentičnosti) {super (provjera autentičnosti); } public boolean isMember (Long OrganizationId) {User user = ((MyUserPrincipal) this.getPrincipal ()). getUser (); vratiti korisnika.getOrganization (). getId (). longValue () == OrganizationId.longValue (); } ...}
5.2. Prilagođeni rukovatelj izrazima
javna klasa CustomMethodSecurityExpressionHandler proširuje DefaultMethodSecurityExpressionHandler {private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl (); @Override zaštićen MethodSecurityExpressionOperations createSecurityExpressionRoot (provjera autentičnosti, zaziv MethodInvocation) {CustomMethodSecurityExpressionRoot root = novi CustomMethodSecurityExpressionRoot (provjera autentičnosti); root.setPermissionEvaluator (getPermissionEvaluator ()); root.setTrustResolver (this.trustResolver); root.setRoleHierarchy (getRoleHierarchy ()); povratni korijen; }}
5.3. Konfiguracija sigurnosti metode
@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) javna klasa MethodSecurityConfig proširuje GlobalMethodSecurityConfiguration {@Override zaštićen MethodSecurityExpressionHandler createExpressionHandler () {CustomMethodSecurityExpressionHandler expressionHandler; expressionHandler.setPermissionEvaluator (novi CustomPermissionEvaluator ()); return expressionHandler; }}
5.4. Korištenje novog izraza
@PreAuthorize ("isMember (#id)") @GetMapping ("/ organization / {id}") @ResponseBody javna organizacija findOrgById (@PathVariable long id) {return organizationRepository.findOne (id); }
5.5. Test uživo
@Test javna praznina givenUserMemberInOrganization_whenGetOrganization_thenOK () {Odgovor odgovora = givenAuth ("john", "123"). Get ("// localhost: 8082 / organization / 1"); assertEquals (200, response.getStatusCode ()); assertTrue (response.asString (). sadrži ("id")); } @Test javna praznina givenUserMemberNotInOrganization_whenGetOrganization_thenForbidden () {Odgovor odgovora = givenAuth ("john", "123"). Get ("// localhost: 8082 / organization / 2"); assertEquals (403, response.getStatusCode ()); }
6. Onemogućite ugrađeni sigurnosni izraz
6.1. Prilagođeni korijen izraza sigurnosti
javna klasa MySecurityExpressionRoot implementira MethodSecurityExpressionOperations {public MySecurityExpressionRoot (Authentication authentication) {if (authentication == null) {throw new IllegalArgumentException ("Objekt autentifikacije ne može biti null"); } this.authentication = provjera autentičnosti; } @Override public final boolean hasAuthority (ovlaštenje niza) {throw new RuntimeException ("metoda hasAuthority () nije dopuštena"); } ...}
6.2. Primjer - Korištenje izraza
@PreAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')") @GetMapping ("/ foos") @ResponseBody public Foo findFooByName (@RequestParam String name) {return new Foo (name); }
6.3. Test uživo
@Test javna praznina givenDisabledSecurityExpression_whenGetFooByName_thenError () {Odgovor odgovora = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos? Name = sample"); assertEquals (500, response.getStatusCode ()); assertTrue (response.asString (). contains ("metoda hasAuthority () nije dopuštena")); }
7. Zaključak