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.

4.4. Test uživo

Napišimo sada jednostavne testove uživo - pogađanje API-ja i provjeravanje je li sve u redu:

@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")); }

I ovdje je naš givenAuth () metoda:

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

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.

5.1. Izražavanje sigurnosti prilagođene metode

Da bismo stvorili ovaj novi prilagođeni izraz, trebamo započeti s implementacijom matične bilješke gdje započinje procjena svih sigurnosnih izraza:

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 (); } ...}

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.

5.2. Prilagođeni rukovatelj izrazima

Dalje, trebamo ubrizgati svoje CustomMethodSecurityExpressionRoot u našem obrađivaču izraza:

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

Sada moramo koristiti svoje CustomMethodSecurityExpressionHandler u konfiguraciji zaštite 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

Evo jednostavnog primjera za zaštitu naše metode kontrolera isMember ():

@PreAuthorize ("isMember (#id)") @GetMapping ("/ organization / {id}") @ResponseBody javna organizacija findOrgById (@PathVariable long id) {return organizationRepository.findOne (id); }

5.5. Test uživo

Na kraju, evo jednostavnog testa za korisnika za korisnika “Ivan“:

@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

Na kraju, pogledajmo kako nadjačati ugrađeni sigurnosni izraz - razgovarat ćemo o onemogućavanju hasAuthority ().

6.1. Prilagođeni korijen izraza sigurnosti

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:

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"); } ...}

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.

6.2. Primjer - Korištenje izraza

Sada, ako želimo koristiti hasAuthority () osigurati metode - kako slijedi, bacit će RuntimeException kada pokušavamo pristupiti metodi:

@PreAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')") @GetMapping ("/ foos") @ResponseBody public Foo findFooByName (@RequestParam String name) {return new Foo (name); }

6.3. Test uživo

Na kraju, evo našeg jednostavnog testa:

@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

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.


$config[zx-auto] not found$config[zx-overlay] not found