使用Spring Security在@WebMvcTest中测试JwtDecoder

时间:2019-11-08 08:21:54

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

我正在使用带有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

也许不使用JwtDecoder,因为Jwt是手动创建的吗?如何确保在测试中调用了JwtDecoder?

3 个答案:

答案 0 :(得分:1)

通过使用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添加其他测试,以确保其正常运行。

答案 1 :(得分: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)在某个Clock类中创建一个@Configuration 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测试切片已经不再启动了(由于使用了工厂bean)。这是一个很好的IMO,否则,我需要为仍未使用的真实JwtDecoder定义属性。结果,我在测试中不再需要controller-test个人资料。

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

@MockBean
private JwtDecoder jwtDecoder;

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

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

答案 2 :(得分:0)

我的猜测是,或者尚未将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。