Inleiding tot Spring Method Security

1. Inleiding

Simpel gezegd ondersteunt Spring Security autorisatiesemantiek op methodeniveau.

Doorgaans kunnen we onze servicelaag beveiligen door bijvoorbeeld te beperken welke rollen een bepaalde methode kunnen uitvoeren - en deze te testen met behulp van speciale beveiligingstestondersteuning op methodniveau.

In dit artikel gaan we eerst het gebruik van enkele beveiligingsannotaties bespreken. Vervolgens zullen we ons concentreren op het testen van onze methodebeveiliging met verschillende strategieën.

2. Methodebeveiliging inschakelen

Om Spring Method Security te gebruiken, moeten we allereerst het spring-security-config afhankelijkheid:

 org.springframework.security spring-security-config 

We kunnen de nieuwste versie vinden op Maven Central.

Als we Spring Boot willen gebruiken, kunnen we de spring-boot-starter-security afhankelijkheid die omvat spring-security-config:

 org.springframework.boot spring-boot-starter-security 

Nogmaals, de nieuwste versie is te vinden op Maven Central.

Vervolgens moeten we globale Method Security inschakelen:

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true, secureEnabled = true, jsr250Enabled = true) openbare klasse MethodSecurityConfig breidt GlobalMethodSecurityConfiguration uit {}
  • De prePostEnabled eigenschap maakt Spring Security pre / post annotaties mogelijk
  • De secureEnabled eigenschap bepaalt of de @Beveiligd annotatie moet zijn ingeschakeld
  • De jsr250 Ingeschakeld property stelt ons in staat om de @RoleAllowed annotatie

In de volgende sectie zullen we meer over deze annotaties onderzoeken.

3. Methodebeveiliging toepassen

3.1. Gebruik makend van @Beveiligd Annotatie

De @Beveiligd annotatie wordt gebruikt om een ​​lijst met rollen op een methode te specificeren. Daarom heeft een gebruiker alleen toegang tot die methode als ze ten minste een van de opgegeven rollen heeft.

Laten we een getUsername methode:

@Secured ("ROLE_VIEWER") openbare String getUsername () {SecurityContext securityContext = SecurityContextHolder.getContext (); retourneer securityContext.getAuthentication (). getName (); }

Hier de @Secured ("ROLE_VIEWER") annotatie definieert dat alleen gebruikers die de rol hebben ROLE_VIEWER zijn in staat om het getUsername methode.

Bovendien kunnen we een lijst met rollen definiëren in een @Beveiligd annotatie:

@Secured ({"ROLE_VIEWER", "ROLE_EDITOR"}) openbare boolean isValidUsername (String gebruikersnaam) {return userRoleRepository.isValidUsername (gebruikersnaam); }

In dit geval stelt de configuratie dat als een gebruiker een van beide heeft ROLE_VIEWER of ROLE_EDITOR, kan die gebruiker het isValidUsername methode.

De @Beveiligd annotatie biedt geen ondersteuning voor Spring Expression Language (SpEL).

3.2. Gebruik makend van @RoleAllowed Annotatie

De @RoleAllowed annotatie is de equivalente annotatie van de JSR-250 van de @Beveiligd annotatie.

In principe kunnen we de @RoleAllowed annotatie op dezelfde manier als @Beveiligd. We zouden dus opnieuw kunnen definiëren getUsername en isValidUsername methoden:

@RolesAllowed ("ROLE_VIEWER") public String getUsername2 () {// ...} @RolesAllowed ({"ROLE_VIEWER", "ROLE_EDITOR"}) public boolean isValidUsername2 (String gebruikersnaam) {// ...}

Evenzo alleen de gebruiker die een rol heeft ROLE_VIEWER kan uitvoeren getUsername2.

Nogmaals, een gebruiker kan een beroep doen isValidUsername2 alleen als ze er tenminste één heeft ROLE_VIEWER of ROLER_EDITOR rollen.

3.3. Gebruik makend van @PreAuthorize en @PostAuthorize Annotaties

Beide @PreAuthorize en @PostAuthorize annotaties bieden toegangscontrole op basis van expressies. Daarom kunnen predikaten worden geschreven met behulp van SpEL (Spring Expression Language).

De @PreAuthorize annotatie controleert de gegeven uitdrukking voordat de methode wordt ingevoerd, terwijl, de @PostAuthorize annotatie verifieert het na de uitvoering van de methode en kan het resultaat wijzigen.

Laten we nu een getUsernameInUpperCase methode zoals hieronder:

@PreAuthorize ("hasRole ('ROLE_VIEWER')") openbare String getUsernameInUpperCase () {retourneer getUsername (). ToUpperCase (); }

De @PreAuthorize ("hasRole (‘ ROLE_VIEWER ') ") heeft dezelfde betekenis als @Secured ("ROLE_VIEWER") die we in de vorige sectie hebben gebruikt. Voel je vrij om meer details over beveiligingsuitdrukkingen te ontdekken in eerdere artikelen.

Bijgevolg is de annotatie @Secured ({"ROLE_VIEWER", "ROLE_EDITOR"}) kan worden vervangen door @PreAuthorize (‘hasRole (‘ ROLE_VIEWER ') of hasRole (‘ROLE_EDITOR')’):

@PreAuthorize ("hasRole ('ROLE_VIEWER') of hasRole ('ROLE_EDITOR')") openbare boolean isValidUsername3 (String gebruikersnaam) {// ...}

Bovendien, we kunnen het methode-argument eigenlijk gebruiken als onderdeel van de uitdrukking:

@PreAuthorize ("# gebruikersnaam == authentication.principal.username") public String getMyRoles (String gebruikersnaam) {// ...}

Hier kan een gebruiker het getMyRoles methode alleen als de waarde van het argument gebruikersnaam is hetzelfde als de gebruikersnaam van de huidige opdrachtgever.

Het is de moeite waard om dat op te merken @PreAuthorize uitdrukkingen kunnen worden vervangen door @PostAuthorize degenen.

Laten we herschrijven getMyRoles:

@PostAuthorize ("# gebruikersnaam == authentication.principal.username") public String getMyRoles2 (String gebruikersnaam) {// ...}

In het vorige voorbeeld zou de autorisatie echter worden vertraagd na de uitvoering van de doelmethode.

Bovendien, de @PostAuthorize annotatie biedt de mogelijkheid om toegang te krijgen tot het resultaat van de methode:

@PostAuthorize ("returnObject.username == authentication.principal.nickName") openbaar CustomUser loadUserDetail (String gebruikersnaam) {return userRoleRepository.loadUserByUserName (gebruikersnaam); }

In dit voorbeeld is de loadUserDetail methode zou alleen succesvol worden uitgevoerd als de gebruikersnaam van de geretourneerde CustomUser is gelijk aan de huidige authenticatie-principal bijnaam.

In deze sectie gebruiken we meestal eenvoudige Spring-uitdrukkingen. Voor complexere scenario's kunnen we aangepaste beveiligingsuitdrukkingen maken.

3.4. Gebruik makend van @PreFilter en @PostFilter Annotaties

Spring Security biedt het @PreFilter annotatie om een ​​verzamelingsargument te filteren voordat de methode wordt uitgevoerd:

@PreFilter ("filterObject! = Authentication.principal.username") public String joinUsernames (Lijst gebruikersnamen) {retourneer usernames.stream (). Collect (Collectors.joining (";")); }

In dit voorbeeld voegen we alle gebruikersnamen toe, behalve degene die is geverifieerd.

Hier, in onze uitdrukking gebruiken we de naam filterObject om het huidige object in de collectie weer te geven.

Als de methode echter meer dan één argument heeft dat een verzamelingstype is, moeten we de filterTarget eigenschap om aan te geven welk argument we willen filteren:

@PreFilter (value = "filterObject! = Authentication.principal.username", filterTarget = "usernames") public String joinUsernamesAndRoles (Lijst gebruikersnamen, Lijst rollen) {return usernames.stream (). Collect (Collectors.joining (";") ) + ":" + rollen.stream (). collect (Collectors.joining (";")); }

Bovendien, we kunnen ook de geretourneerde verzameling van een methode filteren met behulp van @PostFilter annotatie:

@PostFilter ("filterObject! = Authentication.principal.username") openbare lijst getAllUsernamesExceptCurrent () {return userRoleRepository.getAllUsernames (); }

In dit geval de naam filterObject verwijst naar het huidige object in de geretourneerde collectie.

Met die configuratie zal Spring Security de geretourneerde lijst doorlopen en elke waarde verwijderen die overeenkomt met de gebruikersnaam van de opdrachtgever.

Spring Security - Het artikel @PreFilter en @PostFilter beschrijft beide annotaties in meer detail.

3.5. Methode Beveiliging Meta-annotatie

We bevinden ons doorgaans in een situatie waarin we verschillende methoden beschermen met dezelfde beveiligingsconfiguratie.

In dit geval kunnen we een beveiligingsmeta-annotatie definiëren:

@Target (ElementType.METHOD) @Retention (RetentionPolicy.RUNTIME) @PreAuthorize ("hasRole ('VIEWER')") openbaar @interface IsViewer {}

Vervolgens kunnen we de @IsViewer-annotatie direct gebruiken om onze methode te beveiligen:

@IsViewer openbare tekenreeks getUsername4 () {// ...}

Beveiligingsmeta-annotaties zijn een geweldig idee omdat ze meer semantiek toevoegen en onze bedrijfslogica loskoppelen van het beveiligingsraamwerk.

3.6. Beveiligingsannotatie op klassenniveau

Als we merken dat we dezelfde beveiligingsannotatie gebruiken voor elke methode binnen één klasse, kunnen we overwegen om die annotatie op klassenniveau te plaatsen:

@Service @PreAuthorize ("hasRole ('ROLE_ADMIN')") openbare klasse SystemService {openbare tekenreeks getSystemYear () {// ...} openbare tekenreeks getSystemDate () {// ...}}

In het bovenstaande voorbeeld is de beveiligingsregel hasRole (‘ROLE_ADMIN ') wordt op beide toegepast getSystemYear en getSystemDate methoden.

3.7. Meerdere beveiligingsaantekeningen voor een methode

We kunnen ook meerdere beveiligingsannotaties op één methode gebruiken:

@PreAuthorize ("# gebruikersnaam == authentication.principal.username") @PostAuthorize ("returnObject.username == authentication.principal.nickName") openbaar CustomUser secureLoadUserDetail (String gebruikersnaam) {return userRoleRepository.loadUserByUserName (gebruikersnaam); }

Daarom zal Spring de autorisatie verifiëren zowel voor als na de uitvoering van het secureLoadUserDetail methode.

4. Belangrijke overwegingen

Er zijn twee punten die we willen herinneren aan de beveiliging van methoden:

  • Standaard wordt Spring AOP-proxy gebruikt om methodebeveiliging toe te passen - als een beveiligde methode A wordt aangeroepen door een andere methode binnen dezelfde klasse, wordt de beveiliging in A helemaal genegeerd. Dit betekent dat methode A wordt uitgevoerd zonder enige veiligheidscontrole. Hetzelfde geldt voor privémethoden
  • Voorjaar SecurityContext is draadgebonden - standaard wordt de beveiligingscontext niet doorgegeven aan onderliggende threads. Voor meer informatie kunnen we verwijzen naar het Spring Security Context Propagation-artikel

5. Testmethode Beveiliging

5.1. Configuratie

Om Spring Security met JUnit te testen, hebben we het spring-security-test afhankelijkheid:

 org.springframework.security spring-security-test 

We hoeven de afhankelijkheidsversie niet op te geven omdat we de Spring Boot-plug-in gebruiken. We kunnen de nieuwste versie van deze afhankelijkheid vinden op Maven Central.

Laten we vervolgens een eenvoudige Spring Integration-test configureren door de runner en het ApplicationContext configuratie:

@RunWith (SpringRunner.class) @ContextConfiguration openbare klasse MethodSecurityIntegrationTest {// ...}

5.2. Gebruikersnaam en rollen testen

Nu onze configuratie klaar is, gaan we proberen onze getUsername methode die we hebben beveiligd met de @Secured ("ROLE_VIEWER") annotatie:

@Secured ("ROLE_VIEWER") openbare String getUsername () {SecurityContext securityContext = SecurityContextHolder.getContext (); retourneer securityContext.getAuthentication (). getName (); }

Omdat we de @Beveiligd annotatie hier, moet een gebruiker worden geauthenticeerd om de methode aan te roepen. Anders krijgen we een AuthenticationCredentialsNotFoundException.

Vandaar, we hebben een gebruiker nodig om onze beveiligde methode te testen. Om dit te bereiken versieren we de testmethode met @WithMockUser en geef een gebruiker en rollen:

@Test @WithMockUser (gebruikersnaam = "john", rollen = {"VIEWER"}) openbare leegte gegevenRoleViewer_whenCallGetUsername_thenReturnUsername () {String userName = userRoleService.getUsername (); assertEquals ("john", gebruikersnaam); }

We hebben een geverifieerde gebruiker opgegeven wiens gebruikersnaam is John en wiens rol is ROLE_VIEWER. Als we het gebruikersnaam of rol, de standaard gebruikersnaam is gebruiker en standaard rol is ROLE_USER.

Merk op dat het niet nodig is om het ROL_ voorvoegsel hier, zal Spring Security dat voorvoegsel automatisch toevoegen.

Als we dat voorvoegsel niet willen hebben, kunnen we overwegen om Gezag in plaats van rol.

Laten we bijvoorbeeld een getUsernameInLowerCase methode:

@PreAuthorize ("hasAuthority ('SYS_ADMIN')") openbare String getUsernameLC () {return getUsername (). ToLowerCase (); }

We zouden dat kunnen testen met behulp van autoriteiten:

@Test @WithMockUser (gebruikersnaam = "JOHN", autoriteiten = {"SYS_ADMIN"}) openbare ongeldige gegevenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername () {String gebruikersnaam = userRoleService.getUsernameInLowerCase (); assertEquals ("john", gebruikersnaam); }

Handig, als we dezelfde gebruiker voor veel testgevallen willen gebruiken, kunnen we de @WithMockUser annotatie bij testles:

@RunWith (SpringRunner.class) @ContextConfiguration @WithMockUser (gebruikersnaam = "john", rollen = {"VIEWER"}) openbare klasse MockUserAtClassLevelIntegrationTest {// ...}

Als we onze test als anonieme gebruiker wilden uitvoeren, zouden we de @WithAnonymousUser annotatie:

@Test (verwacht = AccessDeniedException.class) @WithAnonymousUser openbare leegte gegevenAnomynousUser_whenCallGetUsername_thenAccessDenied () {userRoleService.getUsername (); }

In het bovenstaande voorbeeld verwachten we een AccessDeniedException omdat de anonieme gebruiker de rol niet heeft gekregen ROLE_VIEWER of de autoriteit SYS_ADMIN.

5.3. Testen met een custom UserDetailsService

Voor de meeste toepassingen is het gebruikelijk om een ​​aangepaste klasse als authenticatie-principal te gebruiken. In dit geval moet de aangepaste klasse het org.springframework.security.core.userdetails.Gebruikersdetails koppel.

In dit artikel verklaren we een CustomUser class die de bestaande implementatie van Gebruikersdetails, dat is org.springframework.security.core.userdetails.Gebruiker:

public class CustomUser breidt User {private String nickname; // getter en setter}

Laten we het voorbeeld terugnemen met de @PostAuthorize annotatie in sectie 3:

@PostAuthorize ("returnObject.username == authentication.principal.nickName") openbaar CustomUser loadUserDetail (String gebruikersnaam) {return userRoleRepository.loadUserByUserName (gebruikersnaam); }

In dit geval zou de methode alleen met succes worden uitgevoerd als de gebruikersnaam van de geretourneerde CustomUser is gelijk aan de huidige authenticatie-principal bijnaam.

Als we die methode wilden testen, kunnen we een implementatie van UserDetailsService die ons zou kunnen laden CustomUser op basis van de gebruikersnaam:

@Test @WithUserDetails (waarde = "john", userDetailsServiceBeanName = "userDetailService") openbare leegte whenJohn_callLoadUserDetail_thenOK () {CustomUser gebruiker = userService.loadUserDetail ("jane"); assertEquals ("jane", user.getNickName ()); }

Hier de @WithUserDetails annotatie stelt dat we een UserDetailsService om onze geverifieerde gebruiker te initialiseren. De dienst wordt verwezen door de userDetailsServiceBeanName eigendom. Dit UserDetailsService kan een echte implementatie zijn of een nep voor testdoeleinden.

Bovendien gebruikt de service de waarde van het onroerend goed waarde als de te laden gebruikersnaam Gebruikersdetails.

Handig kunnen we ook decoreren met een @WithUserDetails annotatie op klassenniveau, vergelijkbaar met wat we deden met de @WithMockUser annotatie.

5.4. Testen met meta-annotaties

We merken vaak dat we dezelfde gebruiker / rollen keer op keer hergebruiken in verschillende tests.

Voor deze situaties is het handig om een meta-annotatie.

Het vorige voorbeeld terugnemen @WithMockUser (gebruikersnaam = ”john”, rollen = {“VIEWER”})kunnen we een meta-annotatie declareren als:

@Retention (RetentionPolicy.RUNTIME) @WithMockUser (value = "john", role = "VIEWER") openbaar @interface WithMockJohnViewer {}

Dan kunnen we gewoon gebruiken @BuienRadarNL in onze test:

@Test @WithMockJohnViewer openbare leegte gegevenMockedJohnViewer_whenCallGetUsername_thenReturnUsername () {String userName = userRoleService.getUsername (); assertEquals ("john", gebruikersnaam); }

Evenzo kunnen we meta-annotaties gebruiken om domeinspecifieke gebruikers te maken met @WithUserDetails.

6. Conclusie

In deze zelfstudie hebben we verschillende opties onderzocht voor het gebruik van Method Security in Spring Security.

We hebben ook een paar technieken doorlopen om de beveiliging van methoden gemakkelijk te testen en hebben geleerd hoe we bespotte gebruikers kunnen hergebruiken in verschillende tests.

Alle voorbeelden van deze tutorial zijn te vinden op Github.


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