Introduktion til Spring Method Security

1. Introduktion

Kort sagt, Spring Security understøtter autorisationssemantik på metodeniveau.

Typisk kunne vi sikre vores servicelag ved f.eks. At begrænse, hvilke roller der er i stand til at udføre en bestemt metode - og teste det ved hjælp af dedikeret supporttest til sikkerhedstest på metodeniveau.

I denne artikel vil vi først gennemgå brugen af ​​nogle sikkerhedsanmærkninger. Derefter vil vi fokusere på at teste vores metodesikkerhed med forskellige strategier.

2. Aktivering af metodesikkerhed

Først og fremmest for at bruge Spring Method Security skal vi tilføje spring-security-config afhængighed:

 org.springframework.security spring-security-config 

Vi kan finde den nyeste version på Maven Central.

Hvis vi vil bruge Spring Boot, kan vi bruge spring-boot-starter-sikkerhed afhængighed, der inkluderer spring-security-config:

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

Igen kan den nyeste version findes på Maven Central.

Dernæst skal vi aktivere global metodesikkerhed:

@Configuration @EnableGlobalMethodSecurity (prePostEnabled = true, secureEnabled = true, jsr250Enabled = true) offentlig klasse MethodSecurityConfig udvider GlobalMethodSecurityConfiguration {}
  • Det prePostEnabled egenskab muliggør annoncer fra foråret efter sikkerhed
  • Det secureEnabled ejendom bestemmer, om @Sikret annotering skal være aktiveret
  • Det jsr250Enabled ejendom tillader os at bruge @RoleAllowed kommentar

Vi undersøger mere om disse kommentarer i det næste afsnit.

3. Anvendelse af metodesikkerhed

3.1. Ved brug af @Sikret Kommentar

Det @Sikret annotation bruges til at specificere en liste over roller på en metode. Derfor kan en bruger kun få adgang til denne metode, hvis hun har mindst en af ​​de angivne roller.

Lad os definere en getUsername metode:

@Secured ("ROLE_VIEWER") offentlig streng getUsername () {SecurityContext securityContext = SecurityContextHolder.getContext (); returner sikkerhedContext.getAuthentication (). getName (); }

Her, den @Secured (“ROLE_VIEWER”) annotation definerer, at kun brugere, der har rollen ROLE_VIEWER er i stand til at udføre getUsername metode.

Desuden kan vi definere en liste over roller i en @Sikret kommentar:

@Secured ({"ROLE_VIEWER", "ROLE_EDITOR"}) offentlig boolsk isValidUsername (streng brugernavn) {return userRoleRepository.isValidUsername (brugernavn); }

I dette tilfælde angiver konfigurationen, at hvis en bruger har en af ​​dem ROLE_VIEWER eller ROLE_EDITOR, kan brugeren påberåbe sig isValidUsername metode.

Det @Sikret annotation understøtter ikke SpEL (Spring Expression Language).

3.2. Ved brug af @RoleAllowed Kommentar

Det @RoleAllowed kommentar er JSR-250s tilsvarende kommentar af @Sikret kommentar.

Dybest set kan vi bruge @RoleAllowed kommentar på en lignende måde som @Sikret. Således kunne vi omdefinere getUsername og isValidUsername metoder:

@RolesAllowed ("ROLE_VIEWER") offentlig streng getUsername2 () {// ...} @RolesAllowed ({"ROLE_VIEWER", "ROLE_EDITOR"}) offentlig boolsk isValidUsername2 (streng brugernavn) {// ...}

Tilsvarende kun den bruger, der har rolle ROLE_VIEWER kan udføre getUsername2.

Igen er en bruger i stand til at påberåbe sig isValidUsername2 kun hvis hun har mindst en af ROLE_VIEWER eller ROLER_EDITOR roller.

3.3. Ved brug af @PreAuthorize og @PostAuthorize Kommentarer

Begge @PreAuthorize og @PostAuthorize annoteringer giver ekspressionsbaseret adgangskontrol. Derfor kan prædikater skrives ved hjælp af SpEL (Spring Expression Language).

Det @PreAuthorize annotation kontrollerer det givne udtryk, inden den går ind i metoden, hvorimod, det @PostAuthorize annotation verificerer det efter udførelsen af ​​metoden og kan ændre resultatet.

Lad os nu erklære en getUsernameInUpperCase metode som nedenfor:

@PreAuthorize ("hasRole ('ROLE_VIEWER')") offentlig streng getUsernameInUpperCase () {returner getUsername (). ToUpperCase (); }

Det @PreAuthorize (“hasRole (‘ ROLE_VIEWER ') ”) har samme betydning som @Secured (“ROLE_VIEWER”) som vi brugte i det foregående afsnit. Du er velkommen til at opdage flere sikkerhedsudtryk detaljer i tidligere artikler.

Følgelig kommentaren @Secured ({“ROLE_VIEWER”, “ROLE_EDITOR”}) kan erstattes med @PreAuthorize ("hasRole ('ROLE_VIEWER') eller hasRole ('ROLE_EDITOR')"):

@PreAuthorize ("hasRole ('ROLE_VIEWER') eller hasRole ('ROLE_EDITOR')") offentlig boolsk isValidUsername3 (String-brugernavn) {// ...}

I øvrigt, vi kan faktisk bruge metodeargumentet som en del af udtrykket:

@PreAuthorize ("# brugernavn == godkendelse.principal.username") offentlig streng getMyRoles (streng brugernavn) {// ...}

Her kan en bruger påberåbe sig getMyRoles metode kun, hvis værdien af ​​argumentet brugernavn er det samme som den aktuelle rektors brugernavn.

Det er værd at bemærke det @PreAuthorize udtryk kan erstattes af @PostAuthorize dem.

Lad os omskrive getMyRoles:

@PostAuthorize ("# brugernavn == godkendelse.principal.username") offentlig streng getMyRoles2 (streng brugernavn) {// ...}

I det foregående eksempel vil autorisationen dog blive forsinket efter udførelsen af ​​målmetoden.

Derudover det @PostAuthorize annotation giver mulighed for at få adgang til metoderesultatet:

@PostAuthorize ("returnObject.username == authentication.principal.nickName") offentlig CustomUser loadUserDetail (streng brugernavn) {return userRoleRepository.loadUserByUserName (brugernavn); }

I dette eksempel er loadUserDetail Metoden ville kun udføres med succes, hvis brugernavn af de returnerede CustomUser er lig med den aktuelle godkendelsesprincipal kaldenavn.

I dette afsnit bruger vi mest enkle forårsudtryk. For mere komplekse scenarier kunne vi oprette brugerdefinerede sikkerhedsudtryk.

3.4. Ved brug af @PreFilter og @PostFilter Kommentarer

Spring Security leverer @PreFilter kommentar for at filtrere et indsamlingsargument, før metoden udføres:

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

I dette eksempel slutter vi os til alle brugernavne undtagen den der er godkendt.

Her, i vores udtryk bruger vi navnet filterObject til at repræsentere det aktuelle objekt i samlingen.

Men hvis metoden har mere end et argument, der er en samlingstype, skal vi bruge filterTarget egenskab for at specificere hvilket argument vi vil filtrere:

@PreFilter (value = "filterObject! = Authentication.principal.username", filterTarget = "usernames") public String joinUsernamesAndRoles (List usernames, Listroller) {return usernames.stream (). Collect (Collectors.joining (";") ) + ":" + roller.stream (). indsamle (Collectors.joining (";")); }

Derudover Vi kan også filtrere den returnerede samling af en metode ved hjælp af @PostFilter kommentar:

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

I dette tilfælde navnet filterObject henviser til det aktuelle objekt i den returnerede samling.

Med denne konfiguration gentages Spring Security gennem den returnerede liste og fjerner enhver værdi, der matcher rektorens brugernavn.

Spring Security - @PreFilter og @PostFilter-artiklen beskriver begge annoteringer mere detaljeret.

3.5. Metodesikkerhed Meta-kommentar

Vi befinder os typisk i en situation, hvor vi beskytter forskellige metoder ved hjælp af den samme sikkerhedskonfiguration.

I dette tilfælde kan vi definere en sikkerhedsmetanotering:

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

Dernæst kan vi direkte bruge @IsViewer-kommentaren til at sikre vores metode:

@IsViewer offentlig streng getUsername4 () {// ...}

Sikkerhedsmetanoteringer er en god idé, fordi de tilføjer mere semantik og afkobler vores forretningslogik fra sikkerhedsrammen.

3.6. Sikkerhedsnotering på klasseniveau

Hvis vi finder os i at bruge den samme sikkerhedsanmærkning til hver metode inden for en klasse, kan vi overveje at placere denne kommentar på klasseniveau:

@Service @PreAuthorize ("hasRole ('ROLE_ADMIN')") offentlig klasse SystemService {public String getSystemYear () {// ...} public String getSystemDate () {// ...}}

I ovenstående eksempel er sikkerhedsreglen hasRole (‘ROLE_ADMIN’) vil blive anvendt på begge dele getSystemYear og getSystemDate metoder.

3.7. Flere sikkerhedsanmærkninger om en metode

Vi kan også bruge flere sikkerhedsanmærkninger på en metode:

@PreAuthorize ("# brugernavn == authentication.principal.username") @PostAuthorize ("returnObject.username == authentication.principal.nickName") offentlig CustomUser secureLoadUserDetail (streng brugernavn) {return userRoleRepository.loadUserByUserName (brugernavn) }

Derfor vil Spring kontrollere godkendelsen både før og efter udførelsen af ​​programmet secureLoadUserDetail metode.

4. Vigtige overvejelser

Der er to punkter, som vi gerne vil minde om metodesikkerhed:

  • Som standard bruges Spring AOP-proxying til at anvende metodesikkerhed - hvis en sikret metode A kaldes efter en anden metode inden for samme klasse, ignoreres sikkerhed i A helt. Dette betyder, at metode A udføres uden nogen sikkerhedskontrol. Det samme gælder private metoder
  • Forår Sikkerhedskontekst er trådbundet - som standard udbredes sikkerhedskonteksten ikke til underordnede tråde. For mere information kan vi henvise til Spring Security Context Propagation-artikel

5. Testmetodesikkerhed

5.1. Konfiguration

For at teste Spring Security med JUnit har vi brug for fjeder-sikkerhed-test afhængighed:

 org.springframework.security spring-security-test 

Vi behøver ikke at specificere afhængighedsversionen, fordi vi bruger Spring Boot-plugin. Vi kan finde den nyeste version af denne afhængighed af Maven Central.

Lad os derefter konfigurere en simpel Spring Integration-test ved at specificere løberen og ApplicationContext konfiguration:

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

5.2. Test af brugernavn og roller

Nu hvor vores konfiguration er klar, lad os prøve at teste vores getUsername metode, som vi sikrede med @Secured (“ROLE_VIEWER”) kommentar:

@Secured ("ROLE_VIEWER") offentlig streng getUsername () {SecurityContext securityContext = SecurityContextHolder.getContext (); returner sikkerhedContext.getAuthentication (). getName (); }

Da vi bruger @Sikret kommentar her, det kræver, at en bruger er godkendt for at påberåbe sig metoden. Ellers får vi en AuthenticationCredentialsNotFoundException.

Derfor, vi er nødt til at give en bruger til at teste vores sikre metode. For at opnå dette dekorerer vi testmetoden med @WithMockUser og give en bruger og roller:

@Test @WithMockUser (brugernavn = "john", roller = {"VIEWER"}) offentligt ugyldigt givenRoleViewer_whenCallGetUsername_thenReturnUsername () {String userName = userRoleService.getUsername (); assertEquals ("john", brugernavn); }

Vi har leveret en godkendt bruger, hvis brugernavn er John og hvis rolle er ROLE_VIEWER. Hvis vi ikke specificerer brugernavn eller rolle, standardindstillingen brugernavn er bruger og standard rolle er ROLE_USER.

Bemærk, at det ikke er nødvendigt at tilføje ROL_ præfiks her, Spring Security tilføjer præfikset automatisk.

Hvis vi ikke vil have dette præfiks, kan vi overveje at bruge myndighed i stedet for rolle.

Lad os f.eks. Erklære en getUsernameInLowerCase metode:

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

Vi kunne teste det ved hjælp af myndigheder:

@Test @WithMockUser (brugernavn = "JOHN", autoriteter = {"SYS_ADMIN"}) offentlig ugyldighed givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername () {String username = userRoleService.getUsernameInLowerCase (); assertEquals ("john", brugernavn); }

Bekvemt Hvis vi vil bruge den samme bruger til mange testsager, kan vi erklære @WithMockUser kommentar ved testklasse:

@RunWith (SpringRunner.class) @ContextConfiguration @WithMockUser (brugernavn = "john", roller = {"VIEWER"}) offentlig klasse MockUserAtClassLevelIntegrationTest {// ...}

Hvis vi ville køre vores test som en anonym bruger, kunne vi bruge @WithAnonymousUser kommentar:

@Test (forventet = AccessDeniedException.class) @WithAnonymousUser offentlig ugyldig givenAnomynousUser_whenCallGetUsername_thenAccessDenied () {userRoleService.getUsername (); }

I eksemplet ovenfor forventer vi en AccessDeniedException fordi den anonyme bruger ikke får rollen ROLE_VIEWER eller autoriteten SYS_ADMIN.

5.3. Test med en brugerdefineret UserDetailsService

For de fleste applikationer er det almindeligt at bruge en brugerdefineret klasse som godkendelsesprincipal. I dette tilfælde skal den tilpassede klasse implementere org.springframework.security.core.userdetails.UserDetails interface.

I denne artikel erklærer vi en CustomUser klasse, der udvider den eksisterende implementering af UserDetails, som er org.springframework.security.core.userdetails.Bruger:

offentlig klasse CustomUser udvider bruger {private String nickname; // getter og setter}

Lad os tage eksemplet tilbage med @PostAuthorize kommentar i afsnit 3:

@PostAuthorize ("returnObject.username == authentication.principal.nickName") offentlig CustomUser loadUserDetail (streng brugernavn) {return userRoleRepository.loadUserByUserName (brugernavn); }

I dette tilfælde udføres metoden kun med succes, hvis brugernavn af de returnerede CustomUser er lig med den aktuelle godkendelsesprincipal kaldenavn.

Hvis vi ville teste den metode, kunne vi give en implementering af UserDetailsService som kunne indlæse vores CustomUser baseret på brugernavnet:

@Test @WithUserDetails (værdi = "john", userDetailsServiceBeanName = "userDetailService") offentlig ugyldig nårJohn_callLoadUserDetail_thenOK () {CustomUser bruger = userService.loadUserDetail ("jane"); assertEquals ("jane", user.getNickName ()); }

Her, den @WithUserDetails annotation siger, at vi bruger en UserDetailsService for at initialisere vores godkendte bruger. Tjenesten henvises af userDetailsServiceBeanName ejendom. Det her UserDetailsService kan være en reel implementering eller en falsk til testformål.

Derudover bruger tjenesten ejendommens værdi værdi som det brugernavn, der skal indlæses UserDetails.

Bekvemt kan vi også dekorere med en @WithUserDetails kommentar på klasseniveau, ligesom hvad vi gjorde med @WithMockUser kommentar.

5.4. Testning med metaanmærkninger

Ofte genbruger vi den samme bruger / roller igen og igen i forskellige tests.

I disse situationer er det praktisk at oprette en meta-kommentar.

At tage det foregående eksempel tilbage @WithMockUser (brugernavn = ”john”, roller = {“VIEWER”}), kan vi erklære en metanotering som:

@Retention (RetentionPolicy.RUNTIME) @WithMockUser (værdi = "john", roller = "VIEWER") offentlig @interface WithMockJohnViewer {}

Så kan vi bare bruge @WithMockJohnViewer i vores test:

@Test @WithMockJohnViewer offentligt ugyldigt givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername () {String userName = userRoleService.getUsername (); assertEquals ("john", brugernavn); }

På samme måde kan vi bruge metanoteringer til at oprette domænespecifikke brugere ved hjælp af @WithUserDetails.

6. Konklusion

I denne vejledning har vi undersøgt forskellige muligheder for at bruge Metodesikkerhed i Spring Security.

Vi har også gennemgået et par teknikker til let at teste metodesikkerhed og lært, hvordan man genbruger spottede brugere i forskellige tests.

Alle eksempler på denne vejledning kan findes på Github.