【问题标题】:Test JwtDecoder in @WebMvcTest with Spring Security使用 Spring Security 在 @WebMvcTest 中测试 JwtDecoder
【发布时间】:2020-03-04 20:50:45
【问题描述】:

我正在使用带有spring-security-oauth2-resource-server:5.2.0.RELEASE 的 Spring Boot 2.2.1。我想写一个集成测试来测试一下安全性是否可以。

我在我的应用程序中定义了这个WebSecurityConfigurerAdapter

import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final OAuth2ResourceServerProperties properties;
    private final SecuritySettings securitySettings;

    public WebSecurityConfiguration(OAuth2ResourceServerProperties properties, SecuritySettings securitySettings) {
        this.properties = properties;
        this.securitySettings = securitySettings;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/**")
            .authenticated()
            .and()
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        NimbusJwtDecoder result = NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri())
                                                  .build();

        OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(
                JwtValidators.createDefault(),
                new AudienceValidator(securitySettings.getApplicationId()));

        result.setJwtValidator(validator);
        return result;
    }

    private static class AudienceValidator implements OAuth2TokenValidator<Jwt> {

        private final String applicationId;

        public AudienceValidator(String applicationId) {
            this.applicationId = applicationId;
        }

        @Override
        public OAuth2TokenValidatorResult validate(Jwt token) {
            if (token.getAudience().contains(applicationId)) {
                return OAuth2TokenValidatorResult.success();
            } else {
                return OAuth2TokenValidatorResult.failure(
                        new OAuth2Error("invalid_token", "The audience is not as expected, got " + token.getAudience(),
                                        null));
            }
        }
    }
}

它有一个自定义验证器来检查令牌中的受众 (aud) 声明。

我目前有这个测试,它有效,但它根本不检查观众声明:

@WebMvcTest(UserController.class)
@EnableConfigurationProperties({SecuritySettings.class, OAuth2ResourceServerProperties.class})
@ActiveProfiles("controller-test")
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testOwnUserDetails() throws Exception {
        mockMvc.perform(get("/api/users/me")
                                .with(jwt(createJwtToken())))
               .andExpect(status().isOk())
               .andExpect(jsonPath("userId").value("AZURE-ID-OF-USER"))
               .andExpect(jsonPath("name").value("John Doe"));
    }

    @Test
    void testOwnUserDetailsWhenNotLoggedOn() throws Exception {
        mockMvc.perform(get("/api/users/me"))
               .andExpect(status().isUnauthorized());
    }

    @NotNull
    private Jwt createJwtToken() {
        String userId = "AZURE-ID-OF-USER";
        String userName = "John Doe";
        String applicationId = "AZURE-APP-ID";

        return Jwt.withTokenValue("fake-token")
                  .header("typ", "JWT")
                  .header("alg", "none")
                  .claim("iss",
                         "https://b2ctestorg.b2clogin.com/80880907-bc3a-469a-82d1-b88ffad655df/v2.0/")
                  .claim("idp", "LocalAccount")
                  .claim("oid", userId)
                  .claim("scope", "user_impersonation")
                  .claim("name", userName)
                  .claim("azp", applicationId)
                  .claim("ver", "1.0")
                  .subject(userId)
                  .audience(Set.of(applicationId))
                  .build();
    }
}

我还有一个 controller-test 配置文件的属性文件,其中包含应用程序 ID 和 jwt-set-uri:

security-settings.application-id=FAKE_ID
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://b2ctestorg.b2clogin.com/b2ctestorg.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_ropc_flow

可能因为 Jwt 是手动创建的,所以没有使用 JwtDecoder?如何确保在测试中调用了 JwtDecoder?

【问题讨论】:

    标签: java spring spring-boot spring-security spring-test


    【解决方案1】:

    为了详细说明 Eleftheria Stein-Kousathana 的答案,我进行了以下更改以使其成为可能:

    1) 创建一个JwtDecoderFactoryBean 类,以便能够对JwtDecoder 和配置的验证器进行单元测试:

    @Component
    public class JwtDecoderFactoryBean implements FactoryBean<JwtDecoder> {
    
        private final OAuth2ResourceServerProperties properties;
        private final SecuritySettings securitySettings;
        private final Clock clock;
    
        public JwtDecoderFactoryBean(OAuth2ResourceServerProperties properties,
                                     SecuritySettings securitySettings,
                                     Clock clock) {
            this.properties = properties;
            this.securitySettings = securitySettings;
            this.clock = clock;
        }
    
    
        @Override
        public JwtDecoder getObject() {
            JwtTimestampValidator timestampValidator = new JwtTimestampValidator();
            timestampValidator.setClock(clock);
            JwtIssuerValidator issuerValidator = new JwtIssuerValidator(securitySettings.getJwtIssuer());
            JwtAudienceValidator audienceValidator = new JwtAudienceValidator(securitySettings.getJwtApplicationId());
            OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(
                    timestampValidator,
                    issuerValidator,
                    audienceValidator);
    
            NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri())
                                                       .build();
    
            decoder.setJwtValidator(validator);
            return decoder;
        }
    
        @Override
        public Class<?> getObjectType() {
            return JwtDecoder.class;
        }
    }
    

    我还将原始代码中的AudienceValidator提取到一个外部类中,并将其重命名为JwtAudienceValidator

    2) 从安全配置中删除JwtDecoder @Bean 方法,使其看起来像这样:

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers("/api/**")
                .authenticated()
                .and()
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        }
    }
    

    3) 在一些@Configuration 类中创建一个Clock bean:

        @Bean
        public Clock clock() {
            return Clock.systemDefaultZone();
        }
    

    (这是对令牌时间到期的单元测试所需要的)

    使用此设置,现在可以为JwtDecoder 设置编写单元测试,这是应用程序使用的实际设置:

    
       // actual @Test methods ommitted, but they can use this private method
       // to setup a JwtDecoder and test some valid/invalid JWT tokens.
    
    @NotNull
        private JwtDecoder createDecoder(String currentTime, String issuer, String audience) {
            OAuth2ResourceServerProperties properties = new OAuth2ResourceServerProperties();
            properties.getJwt().setJwkSetUri(
                    "https://mycompb2ctestorg.b2clogin.com/mycompb2ctestorg.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_ropc_flow");
    
            JwtDecoderFactoryBean factoryBean = new JwtDecoderFactoryBean(properties,
                                                                          new SecuritySettings(audience, issuer),
                                                                          Clock.fixed(Instant.parse(currentTime),
                                                                                      ZoneId.systemDefault()));
            //noinspection ConstantConditions - getObject never returns null in this case
            return factoryBean.getObject();
        }
    

    最后,@WebMvcTest 需要有一个模拟 JwtDecoder,因为不再使用 @WebMvcTest 测试片启动真正的 @WebMvcTest(由于使用工厂 bean)。这是很好的 IMO,否则,我需要为真正的 JwtDecoder 定义属性,但无论如何都没有使用。因此,我在测试中不再需要 controller-test 配置文件。

    所以只需像这样声明一个字段:

    @MockBean
    private JwtDecoder jwtDecoder;
    

    或者创建一个嵌套的测试配置类:

     @TestConfiguration
        static class TestConfig {
            @Bean
            public JwtDecoder jwtDecoder() {
                return mock(JwtDecoder.class);
            }
        }
    

    【讨论】:

      【解决方案2】:

      通过使用 JWT 后处理器 .with(jwt(createJwtToken()))),您可以绕过 JwtDecoder

      考虑如果JwtDecoder 未被绕过会发生什么。
      在过滤器链中,您的请求将到达 JwtDecoder 解析 JWT 值的点。
      在这种情况下,该值为 "fake-token",这将导致异常,因为它不是有效的 JWT。
      这意味着代码甚至不会到达调用AudienceValidator 的位置。

      您可以将传递给SecurityMockMvcRequestPostProcessors.jwt(Jwt jwt) 的值视为将从JwtDecoder.decode(String token) 返回的响应。
      然后,使用SecurityMockMvcRequestPostProcessors.jwt(Jwt jwt) 的测试将测试提供有效 JWT 令牌时的行为。
      您可以为AudienceValidator 添加额外的测试,以确保其正常运行。

      【讨论】:

        【解决方案3】:

        我的猜测是,mockMvc 没有配置为考虑安全方面 (1),或者 @WebMvcTest test slice 没有自动配置所有必需的 bean (2)。

        1:您可以尝试将@AutoConfigureMockMvc 添加到类中,或者使用手动配置mockMvc

        
        @Autowired
        private WebApplicationContext context; 
        
        private MockMvc mockMvc;
        
        @Before
        public void setup() {
        mockMvc = MockMvcBuilders
                        .webAppContextSetup(context)
                        .apply(springSecurity())
                        .build();
        }
        

        2:如果与@WebMvcTest测试切片相关,考虑在测试类中加入@Import(WebSecurityConfig.class)。否则,在测试类上使用@SpringBootTest@AutoConfigureMockMvc 而不是@WebMvcTest 来设置Spring Boot Test。

        【讨论】:

        • 谢谢。我刚刚尝试了您的建议,但没有任何区别。 @AutoConfigurationMockMvc 肯定是不需要的,因为 MockMvc 上的 @Autowired 以前不会起作用。
        猜你喜欢
        • 2021-06-15
        • 2018-05-15
        • 2016-11-05
        • 1970-01-01
        • 1970-01-01
        • 2012-05-10
        • 2017-05-05
        • 1970-01-01
        • 2013-12-21
        相关资源
        最近更新 更多