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.

4.4. Live-testen

La oss nå skrive enkle live-tester - å slå API-en og sørge for at alt er i orden:

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

Og her er vår givenAuth () metode:

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

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.

5.1. Tilpasset metode sikkerhetsuttrykk

For å lage dette nye tilpassede uttrykket, må vi starte med å implementere rotnotatet der evalueringen av alle sikkerhetsuttrykk starter:

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

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å.

5.2. Tilpasset ekspresjonshåndterer

Deretter må vi injisere CustomMethodSecurityExpressionRoot i vår uttrykksbehandler:

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

Nå må vi bruke vår CustomMethodSecurityExpressionHandler i metodesikkerhetskonfigurasjonen:

@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

Her er et enkelt eksempel for å sikre vår kontrollermetode ved hjelp av isMember ():

@PreAuthorize ("isMember (#id)") @GetMapping ("/ organisasjoner / {id}") @ResponseBody offentlig organisasjon findOrgById (@PathVariable lang id) {return organisasjonRepository.findOne (id); }

5.5. Live Test

Til slutt, her er en enkel live test for bruker “john“:

@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

Til slutt, la oss se hvordan vi overstyrer et innebygd sikkerhetsuttrykk - vi diskuterer deaktivering hasAuthority ().

6.1. Tilpasset sikkerhetsuttrykk rot

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:

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

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.

6.2. Eksempel - Bruk av uttrykket

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:

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

6.3. Live Test

Til slutt, her er vår enkle 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

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.


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