Et brugerdefineret sikkerhedsudtryk med forårssikkerhed

1. Oversigt

I denne vejledning fokuserer vi på oprettelse af et brugerdefineret sikkerhedsudtryk med Spring Security.

Nogle gange er de tilgængelige udtryk i rammen simpelthen ikke udtryksfulde nok. Og i disse tilfælde er det relativt simpelt at opbygge et nyt udtryk, der er semantisk rigere end de eksisterende.

Vi diskuterer først, hvordan man opretter en brugerdefineret PermissionEvaluator, derefter et fuldt tilpasset udtryk - og endelig hvordan man tilsidesætter et af det indbyggede sikkerhedsudtryk.

2. En brugerenhed

Lad os først forberede grundlaget for at skabe de nye sikkerhedsudtryk.

Lad os se på vores Bruger enhed - som har en Privilegier og en Organisation:

@Entity offentlig klasse bruger {@Id @GeneratedValue (strategi = GenerationType.AUTO) privat Lang id; @Column (nullable = false, unique = true) private String brugernavn; privat strengadgangskode; @ManyToMany (fetch = FetchType.EAGER) @JoinTable (name = "users_privileges", joinColumns = @JoinColumn (name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn (name = "privilege_id", referencedColumn) id ")) private Set privilegier; @ManyToOne (fetch = FetchType.EAGER) @JoinColumn (name = "organization_id", refereretColumnName = "id") privat organisationsorganisation; // standard getters og setter}

Og her er vores enkle Privilegium:

@Entity public class Privilege {@Id @GeneratedValue (strategi = GenerationType.AUTO) privat Lang id; @Column (nullable = false, unique = true) private String name; // standard getters og setter}

Og vores Organisation:

@Entity offentlig klasse Organisation {@Id @GeneratedValue (strategi = GenerationType.AUTO) privat Lang id; @Column (nullable = false, unique = true) private String name; // standard settere og getters}

Endelig - vi bruger en enklere brugerdefineret Rektor:

offentlig klasse MyUserPrincipal implementerer UserDetails {privat brugerbruger; offentlig MyUserPrincipal (brugerbruger) {this.user = bruger; } @ Override public String getUsername () {return user.getUsername (); } @ Override public String getPassword () {return user.getPassword (); } @ Override offentlig samling getAuthorities () {Liste autoriteter = ny ArrayList (); for (Privilege privilege: user.getPrivileges ()) {autoriteter.add (ny SimpleGrantedAuthority (privilege.getName ())); } returneringsmyndigheder } ...}

Med alle disse klasser klar, skal vi bruge vores brugerdefinerede Rektor i en grundlæggende UserDetailsService implementering:

@Service offentlig klasse MyUserDetailsService implementerer UserDetailsService {@Autowired privat UserRepository userRepository; @ Override offentlige UserDetails loadUserByUsername (String username) {User user = userRepository.findByUsername (username); if (user == null) {throw new UsernameNotFoundException (username); } returner ny MyUserPrincipal (bruger); }}

Som du kan se, er der intet kompliceret ved disse forhold - brugeren har en eller flere privilegier, og hver bruger tilhører en organisation.

3. Opsætning af data

Næste - lad os initialisere vores database med enkle testdata:

@Komponent offentlig klasse SetupData {@Autowired private UserRepository userRepository; @Autowired privat PrivilegeRepository privilegeRepository; @Autowired privat OrganizationRepository organisationRepository; @PostConstruct offentlig ugyldig init () {initPrivileges (); initOrganisations (); initUsers (); }}

Her er vores i det metoder:

privat ugyldigt initPrivileges () {Privilege privilege1 = new Privilege ("FOO_READ_PRIVILEGE"); privilegeRepository.save (privilege1); Privilegium privilegium2 = nyt privilegium ("FOO_WRITE_PRIVILEGE"); privilegeRepository.save (privilege2); }
privat ugyldigt initOrganizations () {Organisation org1 = ny organisation ("FirstOrg"); organizationRepository.save (org1); Organisation org2 = ny organisation ("SecondOrg"); organizationRepository.save (org2); }
private ugyldige initUsers () {Privilege privilege1 = privilegeRepository.findByName ("FOO_READ_PRIVILEGE"); Privilege privilege2 = privilegeRepository.findByName ("FOO_WRITE_PRIVILEGE"); Brugerbruger1 = ny bruger (); user1.setUsername ("john"); bruger1.setPassword ("123"); user1.setPrivileges (nyt HashSet (Arrays.asList (privilege1))); user1.setOrganization (organizationRepository.findByName ("FirstOrg")); userRepository.save (user1); Brugerbruger2 = ny bruger (); user2.setUsername ("tom"); bruger2.setPassword ("111"); user2.setPrivileges (nyt HashSet (Arrays.asList (privilege1, privilege2))); user2.setOrganization (organizationRepository.findByName ("SecondOrg")); userRepository.save (user2); }

Noter det:

  • Bruger “john” har kun FOO_READ_PRIVILEGE
  • Bruger “tom” har begge dele FOO_READ_PRIVILEGE og FOO_WRITE_PRIVILEGE

4. En brugerdefineret tilladelsesevaluator

På dette tidspunkt er vi klar til at begynde at implementere vores nye udtryk - gennem en ny brugerdefineret tilladelsesevaluator.

Vi vil bruge brugerens privilegier til at sikre vores metoder - men i stedet for at bruge hardkodede privilegienavne, vil vi nå en mere åben, fleksibel implementering.

Lad os komme igang.

4.1. PermissionEvaluator

For at oprette vores egen brugerdefinerede tilladelsesevaluator skal vi implementere PermissionEvaluator grænseflade:

offentlig klasse CustomPermissionEvaluator implementerer PermissionEvaluator {@Override public boolean hasPermission (Authentication auth, Object targetDomainObject, Object permission) {if ((auth == null) || (targetDomainObject == null) ||! (tilladelse for String)) {returner 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) ||! (Tilladelse instance of String)) {return false; } return hasPrivilege (auth, targetType.toUpperCase (), permission.toString (). toUpperCase ()); }}

Her er vores hasPrivilege () metode:

private boolean hasPrivilege (Authentication auth, String targetType, String permission) {for (GrantedAuthority givenAuth: auth.getAuthorities ()) {if (givenAuth.getAuthority (). startsWith (targetType)) {if (givenAuth.getAuthority (). indeholder (( tilladelse)) {returner sandt; }}} returner falsk; }

Vi har nu et nyt sikkerhedsudtryk til rådighed og klar til brug: hasPermission.

Og så i stedet for at bruge den mere hardkodede version:

@PostAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')")

Vi kan bruge brugen:

@PostAuthorize ("hasPermission (returnObject, 'læs')")

eller

@PreAuthorize ("hasPermission (#id, 'Foo', 'read')")

Bemærk: #id henviser til metodeparameter og 'Foo'Henviser til målobjektype.

4.2. Metode Sikkerhedskonfiguration

Det er ikke nok til at definere CustomPermissionEvaluator - vi skal også bruge det i vores metodesikkerhedskonfiguration:

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) offentlig klasse MethodSecurityConfig udvider GlobalMethodSecurityConfiguration {@Override-beskyttet MethodSecurityExpressionHandler createExpressionHandler () {DefaultMethodSecurityExpressionHandler expressionHandler = newMethodExpressionHandler; expressionHandler.setPermissionEvaluator (ny CustomPermissionEvaluator ()); return expressionHandler; }}

4.3. Eksempel i praksis

Lad os nu begynde at bruge det nye udtryk - i et par enkle controller-metoder:

@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 klar og bruger det nye udtryk i praksis.

4.4. Live testen

Lad os nu skrive en simpel live test - at ramme API'et og sikre, at alt er i orden:

@Test offentlig ugyldighed givenUserWithReadPrivilegeAndHasPermission_whenGetFooById_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos / 1"); assertEquals (200, respons.getStatusCode ()); assertTrue (respons.asString (). indeholder ("id")); } @Test offentlig ugyldighed givenUserWithNoWritePrivilegeAndHasPermission_whenPostFoo_thenForbidden () {Response response = givenAuth ("john", "123"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (new Foo ("sample //)). foos "); assertEquals (403, respons.getStatusCode ()); } @Test offentlig ugyldighed givenUserWithWritePrivilegeAndHasPermission_whenPostFoo_thenOk () {Response response = givenAuth ("tom", "111"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (new Foo ("sample")) .post ("//h / local) foos "); assertEquals (201, respons.getStatusCode ()); assertTrue (respons.asString (). indeholder ("id")); }

Og her er vores givenAuth () metode:

private RequestSpecification givenAuth (streng brugernavn, streng adgangskode) {FormAuthConfig formAuthConfig = ny FormAuthConfig ("// localhost: 8082 / login", "brugernavn", "adgangskode"); returner RestAssured.given (). auth (). form (brugernavn, adgangskode, formAuthConfig); }

5. Et nyt sikkerhedsudtryk

Med den tidligere løsning var vi i stand til at definere og bruge hasPermission udtryk - hvilket kan være ret nyttigt.

Vi er dog stadig noget begrænset her af navnet og semantikken i selve udtrykket.

Og så, i dette afsnit, vil vi gå fuldt tilpasset - og vi implementerer et sikkerhedsudtryk kaldet isMember () - kontrollere, om hovedmanden er medlem af en organisation.

5.1. Brugertilpasset metode Sikkerhedsudtryk

For at skabe dette nye tilpassede udtryk skal vi starte med at implementere rodnoten, hvor evalueringen af ​​alle sikkerhedsudtryk starter:

offentlig klasse CustomMethodSecurityExpressionRoot udvider 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 (); } ...}

Nu hvordan vi leverede denne nye operation lige i rodnoten her; isMember () bruges til at kontrollere, om den aktuelle bruger er medlem i givet Organisation.

Bemærk også, hvordan vi udvidede SecurityExpressionRoot også at inkludere de indbyggede udtryk.

5.2. Custom Expression Handler

Dernæst skal vi indsprøjte vores CustomMethodSecurityExpressionRoot i vores ekspressionshåndterer:

offentlig klasse CustomMethodSecurityExpressionHandler udvider DefaultMethodSecurityExpressionHandler {privat AuthenticationTrustResolver trustResolver = ny AuthenticationTrustResolverImpl (); @ Override beskyttet MethodSecurityExpressionOperations createSecurityExpressionRoot (Authentication authentication, MethodInvocation invocation) {CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot (authentication); root.setPermissionEvaluator (getPermissionEvaluator ()); root.setTrustResolver (denne.trustResolver); root.setRoleHierarchy (getRoleHierarchy ()); returnere rod; }}

5.3. Metode Sikkerhedskonfiguration

Nu skal vi bruge vores CustomMethodSecurityExpressionHandler i metoden sikkerhedskonfiguration:

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) offentlig klasse MethodSecurityConfig udvider GlobalMethodSecurityConfiguration {@Override-beskyttet MethodSecurityExpressionHandler createExpressionHandler () {CustomMethodSecurityExpressionHandler expressionHandler = new CustomMethod; expressionHandler.setPermissionEvaluator (ny CustomPermissionEvaluator ()); return expressionHandler; }}

5.4. Brug af det nye udtryk

Her er et simpelt eksempel til at sikre vores controller-metode ved hjælp af isMember ():

@PreAuthorize ("isMember (#id)") @GetMapping ("/ organisationer / {id}") @ResponseBody offentlig organisation findOrgById (@PathVariable lang id) {return organisationRepository.findOne (id); }

5.5. Live test

Endelig er her en simpel live test for bruger “John“:

@Test offentlig ugyldighed givenUserMemberInOrganization_whenGetOrganization_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organisationer / 1"); assertEquals (200, respons.getStatusCode ()); assertTrue (respons.asString (). indeholder ("id")); } @Test offentlig ugyldighed givenUserMemberNotInOrganization_whenGetOrganization_thenForbidden () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organisationer / 2"); assertEquals (403, respons.getStatusCode ()); }

6. Deaktiver et indbygget sikkerhedsudtryk

Lad os endelig se, hvordan man tilsidesætter et indbygget sikkerhedsudtryk - vi diskuterer deaktivering hasAuthority ().

6.1. Brugerdefineret sikkerhedsudtryksrod

Vi starter på samme måde ved at skrive vores egne SecurityExpressionRoot - primært fordi de indbyggede metoder er endelig og så kan vi ikke tilsidesætte dem:

public class MySecurityExpressionRoot implementerer MethodSecurityExpressionOperations {public MySecurityExpressionRoot (Authentication authentication) {if (authentication == null) {throw new IllegalArgumentException ("Authentication object may not be null"); } this.authentication = godkendelse; } @ Override offentlig endelig boolesk hasAuthority (streng myndighed) {kast ny RuntimeException ("metode hasAuthority () ikke tilladt"); } ...}

Efter at have defineret denne rodnote, bliver vi nødt til at indsprøjte den i ekspressionshåndteringen og derefter binde denne handler i vores konfiguration - ligesom vi gjorde ovenfor i afsnit 5.

6.2. Eksempel - Brug af udtrykket

Nu, hvis vi vil bruge hasAuthority () for at sikre metoder - som følger kaster det RuntimeException når vi prøver at få adgang til metoden:

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

6.3. Live test

Endelig er her vores enkle test:

@Test offentlig ugyldighed givenDisabledSecurityExpression_whenGetFooByName_thenError () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos? Name = sample"); assertEquals (500, respons.getStatusCode ()); assertTrue (respons.asString (). indeholder ("metoden harAuthority () ikke tilladt")); }

7. Konklusion

I denne vejledning dykkede vi dybt ned i de forskellige måder, hvorpå vi kan implementere et brugerdefineret sikkerhedsudtryk i Spring Security, hvis de eksisterende ikke er nok.

Og som altid kan den fulde kildekode findes på GitHub.


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