Et tilpasset sikkerhetsuttrykk med vårsikkerhet
1. Oversikt
I denne opplæringen vil vi fokusere på skape et tilpasset sikkerhetsuttrykk med Spring Security.
Noen ganger er uttrykkene som er tilgjengelige i rammen rett og slett ikke uttrykksfulle nok. Og i disse tilfellene er det relativt enkelt å bygge opp et nytt uttrykk som er semantisk rikere enn de eksisterende.
Vi vil først diskutere hvordan du lager en egendefinert PermissionEvaluator, deretter et helt tilpasset uttrykk - og til slutt hvordan man overstyrer et av det innebygde sikkerhetsuttrykket.
2. En brukerenhet
La oss først forberede grunnlaget for å skape de nye sikkerhetsuttrykkene.
La oss ta en titt på vår Bruker enhet - som har en Privilegier og en Organisasjon:
@Entity offentlig klasse bruker {@Id @GeneratedValue (strategi = GenerationType.AUTO) privat Lang id; @Column (nullable = false, unique = true) private String brukernavn; privat strengpassord; @ManyToMany (fetch = FetchType.EAGER) @JoinTable (name = "users_privileges", joinColumns = @JoinColumn (name = "user_id", referencedColumnName = "id"), inverseJoinColumn = @JoinColumn (name = "privilege_id", referertColumn) id ")) private Set privilegier; @ManyToOne (fetch = FetchType.EAGER) @JoinColumn (name = "organization_id", referertColumnName = "id") private Organisasjonsorganisasjoner; // standard getters og setters}
Og her er det enkle Privilegium:
@Entity public class Privilege {@Id @GeneratedValue (strategi = GenerationType.AUTO) privat Lang id; @Column (nullable = false, unique = true) private Strengnavn; // standard getters og setters}
Og vår Organisasjon:
@Entity offentlig klasse Organisasjon {@Id @GeneratedValue (strategi = GenerationType.AUTO) privat Lang id; @Column (nullable = false, unique = true) private Strengnavn; // standard setters og getters}
Til slutt - vi bruker en enklere skikk Rektor:
offentlig klasse MyUserPrincipal implementerer UserDetails {privat brukerbruker; offentlig MyUserPrincipal (brukerbruker) {this.user = bruker; } @ Override public String getUsername () {return user.getUsername (); } @ Override public String getPassword () {return user.getPassword (); } @Override public Collection getAuthorities () {Liste autoriteter = ny ArrayList (); for (Privilege privilege: user.getPrivileges ()) {autoriteter.add (ny SimpleGrantedAuthority (privilege.getName ())); } returmyndigheter; } ...}
Med alle disse klassene klare, skal vi bruke vår skikk Rektor i en grunnleggende UserDetailsService gjennomføring:
@Service offentlig klasse MyUserDetailsService implementerer UserDetailsService {@Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername (String username) {User user = userRepository.findByUsername (username); if (user == null) {throw new UsernameNotFoundException (username); } returner ny MyUserPrincipal (bruker); }}
Som du kan se, er det ikke noe komplisert med disse forholdene - brukeren har en eller flere privilegier, og hver bruker tilhører en organisasjon.
3. Dataoppsett
Neste - la oss initialisere databasen vår med enkle testdata:
@Component public class SetupData {@Autowired private UserRepository userRepository; @Autowired private PrivilegeRepository privilegeRepository; @Autowired private OrganizationRepository organizationRepository; @PostConstruct public void init () {initPrivileges (); initOrganizations (); initUsers (); }}
Her er vår i det metoder:
privat ugyldig initPrivileges () {Privilege privilege1 = new Privilege ("FOO_READ_PRIVILEGE"); privilegeRepository.save (privilege1); Privilege privilege2 = new Privilege ("FOO_WRITE_PRIVILEGE"); privilegeRepository.save (privilege2); }
private ugyldige initOrganizations () {Organization org1 = new Organization ("FirstOrg"); organizationRepository.save (org1); Organisasjon org2 = ny organisasjon ("SecondOrg"); organizationRepository.save (org2); }
private ugyldige initUsers () {Privilege privilege1 = privilegeRepository.findByName ("FOO_READ_PRIVILEGE"); Privilege privilege2 = privilegeRepository.findByName ("FOO_WRITE_PRIVILEGE"); Bruker bruker1 = ny bruker (); user1.setUsername ("john"); bruker1.setPassword ("123"); user1.setPrivileges (nytt HashSet (Arrays.asList (privilegium1))); user1.setOrganization (organizationRepository.findByName ("FirstOrg")); userRepository.save (user1); Bruker bruker2 = ny bruker (); user2.setUsername ("tom"); bruker2.setPassword ("111"); user2.setPrivileges (nytt HashSet (Arrays.asList (privilegie1, privilegium2))); user2.setOrganization (organizationRepository.findByName ("SecondOrg")); userRepository.save (user2); }
Noter det:
- Bruker “john” har bare FOO_READ_PRIVILEGE
- Bruker “tom” har begge deler FOO_READ_PRIVILEGE og FOO_WRITE_PRIVILEGE
4. En tilpasset tillatelsesevaluator
På dette punktet er vi klare til å begynne å implementere vårt nye uttrykk - gjennom en ny, tilpasset tillatelsesevaluator.
Vi skal bruke brukerens privilegier for å sikre metodene våre - men i stedet for å bruke hardkodede privilegienavn, vil vi oppnå en mer åpen, fleksibel implementering.
La oss komme i gang.
4.1. PermissionEvaluator
For å lage vår egen tilpassede tillatelsesevaluator må vi implementere PermissionEvaluator grensesnitt:
offentlig klasse CustomPermissionEvaluator implementerer PermissionEvaluator {@Override public boolean hasPermission (Authentication auth, Object targetDomainObject, Object permission) {if ((auth == null) || (targetDomainObject == null) ||! (tillatelse for String)) {return false ; } Streng targetType = targetDomainObject.getClass (). GetSimpleName (). ToUpperCase (); return hasPrivilege (auth, targetType, permission.toString (). toUpperCase ()); } @Override public boolean hasPermission (Authentication auth, Serializable targetId, String targetType, Object permission) {if ((auth == null) || (targetType == null) ||! (Tillatelse for eksempel streng)) {return false; } retur hasPrivilege (auth, targetType.toUpperCase (), permission.toString (). toUpperCase ()); }}
Her er vår hasPrivilege () metode:
private boolean hasPrivilege (Authentication auth, String targetType, String permission) {for (GrantedAuthority givenAuth: auth.getAuthorities ()) {if (givenAuth.getAuthority (). startsWith (targetType)) {if (givenAuth.getAuthority (). inneholder (( tillatelse)) {return true; }}} returner falsk; }
Vi har nå et nytt sikkerhetsuttrykk tilgjengelig og klart til bruk: hasPermission.
Og så, i stedet for å bruke den mer hardkodede versjonen:
@PostAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')")
Vi kan bruke bruk:
@PostAuthorize ("hasPermission (returnObject, 'read')")
eller
@PreAuthorize ("hasPermission (#id, 'Foo', 'read')")
Merk: #id refererer til metodeparameter og ‘Foo‘Refererer til målobjektype.
4.2. Metode Sikkerhetskonfigurasjon
Det er ikke nok å definere CustomPermissionEvaluator - vi må også bruke den i vår metodesikkerhetskonfigurasjon:
@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) offentlig klasse MethodSecurityConfig utvider GlobalMethodSecurityConfiguration {@Override-beskyttet MethodSecurityExpressionHandler createExpressionHandler () {DefaultMethodSecurityExpressionHandler expressionHandlerSand = ExpressionHandler; expressionHandler.setPermissionEvaluator (ny CustomPermissionEvaluator ()); return expressionHandler; }}
4.3. Eksempel i praksis
La oss nå begynne å bruke det nye uttrykket - i noen få enkle kontrollermetoder:
@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, 'skriv')") @PostMapping ("/ foos") @ResponseStatus (HttpStatus.CREATED) @ResponseBody public Foo create (@RequestBody Foo foo) {return foo; }}
Og der går vi - vi er klare og bruker det nye uttrykket i praksis. La oss nå skrive enkle live-tester - å slå API-en og sørge for at alt er i orden: Og her er vår givenAuth () metode: Med den forrige løsningen klarte vi å definere og bruke hasPermission uttrykk - som kan være ganske nyttig. Imidlertid er vi fortsatt noe begrenset her av navnet og semantikken til selve uttrykket. Og så, i denne delen, skal vi gå i full skikk - og vi skal implementere et sikkerhetsuttrykk som heter isMember () - sjekke om rektor er medlem av en organisasjon. For å lage dette nye tilpassede uttrykket, må vi starte med å implementere rotnotatet der evalueringen av alle sikkerhetsuttrykk starter: Nå hvordan vi ga denne nye operasjonen rett i rotnotatet her; isMember () brukes til å sjekke om nåværende bruker er medlem i gitt Organisasjon. Legg også merke til hvordan vi utvidet SecurityExpressionRoot å inkludere de innebygde uttrykkene også. Deretter må vi injisere CustomMethodSecurityExpressionRoot i vår uttrykksbehandler: Nå må vi bruke vår CustomMethodSecurityExpressionHandler i metodesikkerhetskonfigurasjonen: Her er et enkelt eksempel for å sikre vår kontrollermetode ved hjelp av isMember (): Til slutt, her er en enkel live test for bruker “john“: Til slutt, la oss se hvordan vi overstyrer et innebygd sikkerhetsuttrykk - vi diskuterer deaktivering hasAuthority (). Vi starter på samme måte med å skrive våre egne SecurityExpressionRoot - hovedsakelig fordi de innebygde metodene er endelig og så kan vi ikke overstyre dem: Etter å ha definert denne rotnoten, må vi injisere den i uttrykksbehandleren og deretter koble den håndtereren til vår konfigurasjon - akkurat som vi gjorde ovenfor i avsnitt 5. Nå, hvis vi vil bruke hasAuthority () for å sikre metoder - som følger, vil det kaste RuntimeException når vi prøver å få tilgang til metoden: Til slutt, her er vår enkle test: I denne guiden gjorde vi et dypdykk i de forskjellige måtene vi kan implementere et tilpasset sikkerhetsuttrykk i Spring Security, hvis de eksisterende ikke er nok. Og som alltid kan hele kildekoden finnes på GitHub.4.4. Live-testen
@Test public void givenUserWithReadPrivilegeAndHasPermission_whenGetFooById_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos / 1"); assertEquals (200, respons.getStatusCode ()); assertTrue (respons.asString (). inneholder ("id")); } @Test offentlig ugyldig givenUserWithNoWritePrivilegeAndHasPermission_whenPostFoo_thenForbidden () {Response response = givenAuth ("john", "123"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (new Foo ("sample //) /). foos "); assertEquals (403, respons.getStatusCode ()); } @Test offentlig ugyldig givenUserWithWritePrivilegeAndHasPermission_whenPostFoo_thenOk () {Response response = givenAuth ("tom", "111"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (new Foo ("sample")) .post ("//h / local) foos "); assertEquals (201, response.getStatusCode ()); assertTrue (respons.asString (). inneholder ("id")); }
private RequestSpecification givenAuth (String brukernavn, String passord) {FormAuthConfig formAuthConfig = new FormAuthConfig ("// localhost: 8082 / login", "username", "password"); returner RestAssured.given (). auth (). skjema (brukernavn, passord, formAuthConfig); }
5. Et nytt sikkerhetsuttrykk
5.1. Tilpasset metode sikkerhetsuttrykk
offentlig klasse CustomMethodSecurityExpressionRoot utvider SecurityExpressionRoot implementerer MethodSecurityExpressionOperations {public CustomMethodSecurityExpressionRoot (Authentication authentication) {super (authentication); } public boolean isMember (Long OrganizationId) {User user = ((MyUserPrincipal) this.getPrincipal ()). getUser (); returner user.getOrganization (). getId (). longValue () == OrganizationId.longValue (); } ...}
5.2. Tilpasset ekspresjonshåndterer
offentlig klasse CustomMethodSecurityExpressionHandler utvider DefaultMethodSecurityExpressionHandler {private AuthenticationTrustResolver trustResolver = ny AuthenticationTrustResolverImpl (); @ Override-beskyttet MethodSecurityExpressionOperations createSecurityExpressionRoot (Autentiseringsautentisering, MethodInvocation-anrop) {CustomMethodSecurityExpressionRoot root = ny CustomMethodSecurityExpressionRoot (autentisering); root.setPermissionEvaluator (getPermissionEvaluator ()); root.setTrustResolver (this.trustResolver); root.setRoleHierarchy (getRoleHierarchy ()); returrot; }}
5.3. Metode Sikkerhetskonfigurasjon
@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) offentlig klasse MethodSecurityConfig utvider GlobalMethodSecurityConfiguration {@ Override-beskyttet MethodSecurityExpressionHandler createExpressionHandler () {CustomMethodSecurityExpressionHandler expressionHandler = new CustomMethod; expressionHandler.setPermissionEvaluator (ny CustomPermissionEvaluator ()); return expressionHandler; }}
5.4. Bruke det nye uttrykket
@PreAuthorize ("isMember (#id)") @GetMapping ("/ organisasjoner / {id}") @ResponseBody offentlig organisasjon findOrgById (@PathVariable lang id) {return organisasjonRepository.findOne (id); }
5.5. Live Test
@Test public void givenUserMemberInOrganization_whenGetOrganization_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organisasjoner / 1"); assertEquals (200, respons.getStatusCode ()); assertTrue (respons.asString (). inneholder ("id")); } @Test offentlig ugyldighet gittUserMemberNotInOrganization_whenGetOrganization_thenForbidden () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organisasjoner / 2"); assertEquals (403, response.getStatusCode ()); }
6. Deaktiver et innebygd sikkerhetsuttrykk
6.1. Tilpasset sikkerhetsuttrykk rot
offentlig klasse MySecurityExpressionRoot implementerer MethodSecurityExpressionOperations {public MySecurityExpressionRoot (Authentication authentication) {if (authentication == null) {throw new IllegalArgumentException ("Autentiseringsobjekt kan ikke være null"); } this.authentication = autentisering; } @ Override offentlig endelig boolsk hasAuthority (streng autoritet) {kast ny RuntimeException ("metoden hasAuthority () ikke tillatt"); } ...}
6.2. Eksempel - Bruk av uttrykket
@PreAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')") @GetMapping ("/ foos") @ResponseBody public Foo findFooByName (@RequestParam String name) {return new Foo (name); }
6.3. Live Test
@Test public void givenDisabledSecurityExpression_whenGetFooByName_thenError () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos? Name = sample"); assertEquals (500, respons.getStatusCode ()); assertTrue (respons.asString (). inneholder ("metoden harAuthority () ikke tillatt")); }
7. Konklusjon