Een aangepaste beveiligingsuitdrukking met Spring Security

1. Overzicht

In deze tutorial zullen we ons concentreren op een eigen beveiligingsexpressie creëren met Spring Security.

Soms zijn de uitdrukkingen die in het raamwerk beschikbaar zijn simpelweg niet expressief genoeg. En in deze gevallen is het relatief eenvoudig om een ​​nieuwe uitdrukking op te bouwen die semantisch rijker is dan de bestaande.

We zullen eerst bespreken hoe u een maatwerk kunt maken Toestemming Evaluator, vervolgens een volledig aangepaste expressie - en tot slot hoe u een van de ingebouwde beveiligingsexpressies kunt negeren.

2. Een gebruikersentiteit

Laten we eerst de basis voorbereiden voor het maken van de nieuwe beveiligingsuitdrukkingen.

Laten we eens kijken naar onze Gebruiker entiteit - die een Privileges en een Organisatie:

@Entity openbare klasse Gebruiker {@Id @GeneratedValue (strategie = GenerationType.AUTO) privé Lange id; @Column (nullable = false, unique = true) private String gebruikersnaam; privé String-wachtwoord; @ManyToMany (fetch = FetchType.EAGER) @JoinTable (naam = "gebruikers_privileges", joinColumns = @JoinColumn (naam = "gebruiker_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn = @JoinColumn = "privilege_id", naam = "privilege_id", id ")) private Set privileges; @ManyToOne (fetch = FetchType.EAGER) @JoinColumn (naam = "organisation_id", referencedColumnName = "id") private organisatie-organisatie; // standaard getters en setters}

En hier is onze simpele Voorrecht:

@ Entity public class Privilege {@Id @GeneratedValue (strategy = GenerationType.AUTO) privé Lange id; @Column (nullable = false, unique = true) private String naam; // standaard getters en setters}

En onze Organisatie:

@Entity openbare klasse Organisatie {@Id @GeneratedValue (strategy = GenerationType.AUTO) privé Lange id; @Column (nullable = false, unique = true) private String naam; // standaard setters en getters}

Ten slotte gebruiken we een eenvoudiger gebruik Opdrachtgever:

openbare klasse MyUserPrincipal implementeert UserDetails {private User user; openbare MyUserPrincipal (gebruiker gebruiker) {this.user = gebruiker; } @Override public String getUsername () {return user.getUsername (); } @Override public String getPassword () {return user.getPassword (); } @Override openbare verzameling getAuthorities () {Lijst autoriteiten = nieuwe ArrayList (); voor (Privilege privilege: user.getPrivileges ()) {autoriteiten.add (nieuwe SimpleGrantedAuthority (privilege.getName ())); } terugkeerautoriteiten; } ...}

Nu al deze lessen klaar zijn, gaan we onze gewoonte gebruiken Opdrachtgever in een basic UserDetailsService implementatie:

@Service openbare klasse MyUserDetailsService implementeert UserDetailsService {@Autowired privé UserRepository userRepository; @Override openbare UserDetails loadUserByUsername (String gebruikersnaam) {User user = userRepository.findByUsername (gebruikersnaam); if (user == null) {throw nieuwe UsernameNotFoundException (gebruikersnaam); } retourneer nieuwe MyUserPrincipal (gebruiker); }}

Zoals u kunt zien, is er niets ingewikkelds aan deze relaties: de gebruiker heeft een of meer rechten en elke gebruiker behoort tot één organisatie.

3. Gegevensinstellingen

Vervolgens - laten we onze database initialiseren met eenvoudige testgegevens:

@Component openbare klasse SetupData {@Autowired privé UserRepository userRepository; @Autowired privé PrivilegeRepository privilegeRepository; @Autowired privé OrganizationRepository organisationRepository; @PostConstruct public void init () {initPrivileges (); initOrganizations (); initUsers (); }}

Hier is onze in het methoden:

private void initPrivileges () {Privilege privilege1 = nieuw privilege ("FOO_READ_PRIVILEGE"); privilegeRepository.save (privilege1); Privilege privilege2 = nieuw privilege ("FOO_WRITE_PRIVILEGE"); privilegeRepository.save (privilege2); }
private void initOrganizations () {Organisatie org1 = nieuwe organisatie ("FirstOrg"); organisatieRepository.save (org1); Organisatie org2 = nieuwe organisatie ("SecondOrg"); organisatieRepository.save (org2); }
private void initUsers () {Privilege privilege1 = privilegeRepository.findByName ("FOO_READ_PRIVILEGE"); Privilege privilege2 = privilegeRepository.findByName ("FOO_WRITE_PRIVILEGE"); Gebruiker user1 = nieuwe gebruiker (); user1.setUsername ("john"); user1.setPassword ("123"); user1.setPrivileges (nieuwe HashSet (Arrays.asList (privilege1))); user1.setOrganization (organisationRepository.findByName ("FirstOrg")); userRepository.save (gebruiker1); Gebruiker user2 = nieuwe gebruiker (); user2.setUsername ("tom"); user2.setPassword ("111"); user2.setPrivileges (nieuwe HashSet (Arrays.asList (privilege1, privilege2))); user2.setOrganization (organisationRepository.findByName ("SecondOrg")); userRepository.save (gebruiker2); }

Let daar op:

  • Gebruiker "john" heeft alleen FOO_READ_PRIVILEGE
  • Gebruiker "tom" heeft beide FOO_READ_PRIVILEGE en FOO_WRITE_PRIVILEGE

4. Een Custom Permission Evaluator

Op dit punt zijn we klaar om onze nieuwe expressie te implementeren - via een nieuwe, aangepaste toestemmingsevaluator.

We gaan de gebruikersrechten gebruiken om onze methoden te beveiligen - maar in plaats van hard gecodeerde privilege-namen te gebruiken, willen we een meer open, flexibele implementatie bereiken.

Laten we beginnen.

4.1. Toestemming Evaluator

Om onze eigen aangepaste toestemmingsevaluator te maken, moeten we de Toestemming Evaluator koppel:

public class CustomPermissionEvaluator implementeert PermissionEvaluator {@Override public boolean hasPermission (authenticatie auth, Object targetDomainObject, Object permissie) {if ((auth == null) || (targetDomainObject == null) ||! (permissie instantie van String)) {return false ; } String targetType = targetDomainObject.getClass (). GetSimpleName (). ToUpperCase (); return hasPrivilege (auth, targetType, permissie.toString (). toUpperCase ()); } @Override public boolean hasPermission (Authentication auth, Serializable targetId, String targetType, Object permissie) {if ((auth == null) || (targetType == null) ||! (Permissie instantie van String)) {return false; } return hasPrivilege (auth, targetType.toUpperCase (), permissie.toString (). toUpperCase ()); }}

Hier is onze hasPrivilege () methode:

private boolean hasPrivilege (Authentication auth, String targetType, String toestemming) {for (GrantedAuthority verleendAuth: auth.getAuthorities ()) {if (verleendAuth.getAuthority (). startsWith (targetType)) {if (GrantAuth.getAuthority (). bevat ( toestemming)) {retourneer waar; }}} return false; }

We hebben nu een nieuwe beveiligingsuitdrukking beschikbaar en klaar voor gebruik: hasPermission.

En dus, in plaats van de meer hardgecodeerde versie te gebruiken:

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

We kunnen gebruik maken van:

@PostAuthorize ("hasPermission (returnObject, 'read')")

of

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

Opmerking: #ID kaart verwijst naar methodeparameter en ‘Foo‘Verwijst naar het type doelobject.

4.2. Methode Beveiligingsconfiguratie

Het is niet genoeg om de CustomPermissionEvaluator - we moeten het ook gebruiken in de beveiligingsconfiguratie van onze methode:

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) openbare klasse MethodSecurityConfig breidt GlobalMethodSecurityConfiguration uit {@Override protected MethodSecurityExpressionHandler createExpressionHandler () {DefaultMethodSecurityExpressionHandler expressionHandler = newurity; expressionHandler.setPermissionEvaluator (nieuwe CustomPermissionEvaluator ()); return expressionHandler; }}

4.3. Voorbeeld in de praktijk

Laten we nu beginnen met het gebruiken van de nieuwe uitdrukking - in een paar eenvoudige controllermethoden:

@Controller openbare klasse MainController {@PostAuthorize ("hasPermission (returnObject, 'read')") @GetMapping ("/ foos / {id}") @ResponseBody openbare Foo findById (@PathVariable lange id) {return new Foo ("Sample "); } @PreAuthorize ("hasPermission (#foo, 'write')") @PostMapping ("/ foos") @ResponseStatus (HttpStatus.CREATED) @ResponseBody public Foo create (@RequestBody Foo foo) {return foo; }}

En daar gaan we - we zijn helemaal klaar en gebruiken de nieuwe uitdrukking in de praktijk.

4.4. De live test

Laten we nu een eenvoudige live-test schrijven - de API gebruiken en ervoor zorgen dat alles in orde is:

@Test openbare leegte gegevenUserWithReadPrivilegeAndHasPermission_whenGetFooById_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos / 1"); assertEquals (200, response.getStatusCode ()); assertTrue (response.asString (). bevat ("id")); } @Test openbare leegte gegevenUserWithNoWritePrivilegeAndHasPermission_whenPostFoo_thenForbidden () {Response response = givenAuth ("john", "123"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (nieuwe Foo ("sample")) .post ("// local")) .post ("// local" foos "); assertEquals (403, response.getStatusCode ()); } @Test openbare leegte gegevenUserWithWritePrivilegeAndHasPermission_whenPostFoo_thenOk () {Response response = givenAuth ("tom", "111"). ContentType (MediaType.APPLICATION_JSON_VALUE) .body (nieuwe Foo ("sample")) .post ("// localhost: 8082 / foos "); assertEquals (201, response.getStatusCode ()); assertTrue (response.asString (). bevat ("id")); }

En hier is onze gegevenAuth () methode:

private RequestSpecification givenAuth (String gebruikersnaam, String wachtwoord) {FormAuthConfig formAuthConfig = new FormAuthConfig ("// localhost: 8082 / login", "gebruikersnaam", "wachtwoord"); retourneer RestAssured.given (). auth (). form (gebruikersnaam, wachtwoord, formAuthConfig); }

5. Een nieuwe beveiligingsuitdrukking

Met de vorige oplossing konden we de hasPermission expressie - wat best handig kan zijn.

We zijn hier echter nog steeds enigszins beperkt door de naam en semantiek van de uitdrukking zelf.

En dus gaan we in deze sectie volledig op maat - en we gaan een beveiligingsexpressie implementeren met de naam isMember () - controleren of de opdrachtgever lid is van een organisatie.

5.1. Custom Method Security Expression

Om deze nieuwe aangepaste expressie te maken, moeten we beginnen met het implementeren van de grondtoon waar de evaluatie van alle beveiligingsuitdrukkingen begint:

public class CustomMethodSecurityExpressionRoot breidt SecurityExpressionRoot uit implementeert MethodSecurityExpressionOperations {public CustomMethodSecurityExpressionRoot (authenticatie authenticatie) {super (authenticatie); } openbare boolean isMember (Long OrganizationId) {User user = ((MyUserPrincipal) this.getPrincipal ()). getUser (); return user.getOrganization (). getId (). longValue () == OrganizationId.longValue (); } ...}

Nu, hoe we deze nieuwe bewerking hier in de grondtoon hebben aangebracht; isMember () wordt gebruikt om te controleren of de huidige gebruiker lid is van een gegeven Organisatie.

Merk ook op hoe we het BeveiligingExpressionRoot om ook de ingebouwde uitdrukkingen op te nemen.

5.2. Aangepaste expressie-handler

Vervolgens moeten we onze CustomMethodSecurityExpressionRoot in onze expressie-handler:

openbare klasse CustomMethodSecurityExpressionHandler breidt DefaultMethodSecurityExpressionHandler {privé AuthenticationTrustResolver trustResolver = nieuwe AuthenticationTrustResolverImpl () uit; @Override beschermde MethodSecurityExpressionOperations createSecurityExpressionRoot (Authenticatie authenticatie, MethodInvocation aanroep) {CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot (authenticatie); root.setPermissionEvaluator (getPermissionEvaluator ()); root.setTrustResolver (this.trustResolver); root.setRoleHierarchy (getRoleHierarchy ()); terugkeer root; }}

5.3. Methode Beveiligingsconfiguratie

Nu moeten we onze CustomMethodSecurityExpressionHandler in de beveiligingsconfiguratie van de methode:

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true) openbare klasse MethodSecurityConfig breidt GlobalMethodSecurityConfiguration uit {@Override protected MethodSecurityExpressionHandler createExpressionHandler () {CustomMethodSecurityExpressionHandler expressionHandler = (CustomMethodSecurityExpressionHandler expressionHandler = (CustomMethodler) expressionHandler.setPermissionEvaluator (nieuwe CustomPermissionEvaluator ()); return expressionHandler; }}

5.4. Met behulp van de nieuwe uitdrukking

Hier is een eenvoudig voorbeeld om onze controllermethode te beveiligen met isMember ():

@PreAuthorize ("isMember (#id)") @GetMapping ("/ organisaties / {id}") @ResponseBody openbare organisatie findOrgById (@PathVariable lange id) {return organisationRepository.findOne (id); }

5.5. Live test

Eindelijk is hier een eenvoudige live test voor de gebruiker "John“:

@Test openbare leegte gegevenUserMemberInOrganization_whenGetOrganization_thenOK () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organisaties / 1"); assertEquals (200, response.getStatusCode ()); assertTrue (response.asString (). bevat ("id")); } @Test openbare leegte gegevenUserMemberNotInOrganization_whenGetOrganization_thenForbidden () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / organisaties / 2"); assertEquals (403, response.getStatusCode ()); }

6. Schakel een ingebouwde beveiligingsuitdrukking uit

Laten we tot slot kijken hoe we een ingebouwde beveiligingsexpressie kunnen negeren - we zullen het uitschakelen bespreken hasAuthority ().

6.1. Aangepaste beveiligingsexpressie Root

We beginnen op dezelfde manier door onze eigen te schrijven BeveiligingExpressionRoot - vooral omdat de ingebouwde methoden dat zijn laatste en dus kunnen we ze niet negeren:

openbare klasse MySecurityExpressionRoot implementeert MethodSecurityExpressionOperations {openbare MySecurityExpressionRoot (authenticatie-authenticatie) {if (authentication == null) {throw new IllegalArgumentException ("Authenticatie-object mag niet null zijn"); } this.authentication = authenticatie; } @Override public final boolean hasAuthority (String autoriteit) {throw new RuntimeException ("methode hasAuthority () niet toegestaan"); } ...}

Nadat we deze grondtoon hebben gedefinieerd, moeten we deze in de expressiehandler injecteren en vervolgens die handler in onze configuratie aansluiten - net zoals we hierboven in sectie 5 hebben gedaan.

6.2. Voorbeeld - de uitdrukking gebruiken

Nu, als we willen gebruiken hasAuthority () om methoden te beveiligen - als volgt, het zal gooien RuntimeException wanneer we proberen toegang te krijgen tot de methode:

@PreAuthorize ("hasAuthority ('FOO_READ_PRIVILEGE')") @GetMapping ("/ foos") @ResponseBody openbare Foo findFooByName (@RequestParam String naam) {retourneer nieuwe Foo (naam); }

6.3. Live test

Eindelijk is hier onze eenvoudige test:

@Test openbare leegte gegevenDisabledSecurityExpression_whenGetFooByName_thenError () {Response response = givenAuth ("john", "123"). Get ("// localhost: 8082 / foos? Name = sample"); assertEquals (500, response.getStatusCode ()); assertTrue (response.asString (). bevat ("methode hasAuthority () niet toegestaan")); }

7. Conclusie

In deze handleiding hebben we dieper ingegaan op de verschillende manieren waarop we een aangepaste beveiligingsexpressie in Spring Security kunnen implementeren, als de bestaande niet voldoende zijn.

En, zoals altijd, is de volledige broncode te vinden op GitHub.