簡體   English   中英

Spring 引導 - 如何對 @Service class 方法在使用 JWT 和自定義聲明時使用 @PreAuthorize 進行單元測試

[英]Spring Boot - How to unit test @Service class methods that use @PreAuthorize when using JWT with custom claims

我們有一個 Spring 引導后端應用程序,它使用 Spring 安全性和 OAuth2 資源服務器實現了 JWT 身份驗證。 我們通過使用antMatchers()根據用戶角色和使用全局方法安全性的服務層方法限制對 URL 模式的訪問來保護我們的 web 層。

我們的 JWT 令牌由 Auth0 提供,我們在其中配置為包含額外的用戶元數據作為自定義聲明。 這些聲明值在@PreAuthorize注釋中進行驗證,例如:

    @PreAuthorize("authentication.principal.claims[@environment.getProperty('jwt.organizationIdClaim')] == @environment.getProperty('current.organizationId')")
    public void deleteUser(String userId) {

這里的屬性jwt.organizationIdClaimcurrent.organizationId是從 application.properties 文件中獲取的自定義屬性。

當我嘗試對上述方法進行單元測試時,出現如下異常:

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext

單元測試代碼:

    @Test
    void testDoNotAllowDeletingUsersWithRootRole() {
        auth0ManagementAPIService.deleteUser("auth0|REDACTED");
        assertTrue(true);
    }

我嘗試使用@WithMockUser運行此測試,但這導致無法評估 @PreAuthorize 表達式:

java.lang.IllegalArgumentException: Failed to evaluate expression 'authentication.principal.claims[@environment.getProperty('jwt.organizationIdClaim')] == @environment.getProperty('current.organizationId')'

有沒有辦法將這些自定義聲明數據添加到這些單元測試的SecurityContext中,以便 @PreAuthorize 表達式成功評估?

我們使用SecurityMockMvcRequestPostProcessors.jwt()繞過了 web 層中的 JWT 要求,也許我們可以將類似的東西用於@Service ZA2F2ED4F8EBC2CBB4C21A29DC40AB61Z 方法?

我們使用的 JWT 的示例負載如下所示:

{
  "https://dev.api.REDACTED.com/roles": [
    "RESEARCHER",
    "ADMIN",
    "ROOT"
  ],
  "https://dev.api.REDACTED.com/organizationId": "REDACTED",
  "https://dev.api.REDACTED.com/userId": "REDACTED",
  "iss": "https://dev.login.REDACTED.com/",
  "sub": "auth0|REDACTED",
  "aud": [
    "https://dev.api.REDACTED.com",
    "https://REDACTED.us.auth0.com/userinfo"
  ],
  "iat": 1660626647,
  "exp": 1660713047,
  "azp": "REDACTED",
  "scope": "openid profile email offline_access",
  "permissions": [
    ...
  ]
}

Here https://dev.api.REDACTED.com/roles , https://dev.api.REDACTED.com/organizationId and https://dev.api.REDACTED.com/userId are custom claims whose values we need to在@PreAuthorize 條件中進行驗證。

主體 object 的類型為 Jwt,使用自定義轉換器生成如下:

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

import java.util.ArrayList;
import java.util.List;

public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    private final String rolesClaimName;

    public CustomJwtAuthenticationConverter(String rolesClaimName) {
        this.rolesClaimName = rolesClaimName;
    }

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        // Get roles claim from the JWT token and add it to a granted authorities list.
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        List<String> rolesList = jwt.getClaimAsStringList(rolesClaimName);
        rolesList.forEach(r -> grantedAuthorities.add(new SimpleGrantedAuthority(r)));
        return new JwtAuthenticationToken(jwt, grantedAuthorities, jwt.getSubject());
    }
}

然后將其注入安全配置中的安全過濾器鏈:

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(customJwtGrantedAuthoritiesConverter());
        http
                .cors().and()
                .authorizeRequests(
                        authorize -> authorize
                                .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                                .antMatchers("/actuator", "/actuator/health").permitAll()
                                .antMatchers(HttpMethod.DELETE, "/users/**").hasAuthority(ROOT_ROLE)
                                .antMatchers("/users/**").hasAnyAuthority(ADMIN_ROLE, ROOT_ROLE)
                                .anyRequest().authenticated());
        return http.build();
    }

    @Bean
    public Converter<Jwt, AbstractAuthenticationToken> customJwtGrantedAuthoritiesConverter() {
        return new CustomJwtAuthenticationConverter(rolesClaimName);
    }

我有你需要的東西: https://github.com/ch4mpy/spring-addons

樣品在這里

    @Test
    @WithMockJwtAuth(authorities = { "NICE", "AUTHOR" }, claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"))
    void whenGrantedWithNiceRoleThenCanGreet() throws Exception {
        final var actual = mySecuredService.returnSomething();
        //test return falue
    }

@OpenIdClaims允許您配置標准 OpenID 聲明(但沒有一個是強制性的)和您喜歡的任何私有聲明。

測試注釋相對於請求后處理器的優勢在於它可以在沒有 MockMvc 的情況下工作(當您想要對不是@Controller的安全@Component進行單元測試時會發生這種情況,例如@Service@Repository )。 不幸的是,spring-security 團隊在我貢獻 OAuth2 MockMvc 后處理器(和 WebTestClient 變異器)時對它不感興趣。 我啟動上面鏈接的庫的原因。

附言

您可能會在其他教程中找到有用的想法。 resource-server_with_oauthentication可以為您節省相當多的配置代碼, resource-server_with_specialized_oauthentication可以幫助您提高安全表達式的可讀性。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM