如何在 Spring Boot 单元测试中模拟 JWT 身份验证?

新手上路,请多包涵

我已按照 此示例 将使用 Auth0 的 JWT 身份验证添加到我的 Spring Boot REST API 中。

现在,正如预期的那样,我之前的工作 Controller 单元测试给出了响应代码 401 Unauthorized 而不是 200 OK 因为我没有在测试中通过任何 JWT。

我如何模拟 JWT/Authentication 我的 REST 控制器测试的一部分?

单元测试类

@AutoConfigureMockMvc
public class UserRoundsControllerTest extends AbstractUnitTests {

    private static String STUB_USER_ID = "user3";
    private static String STUB_ROUND_ID = "7e3b270222252b2dadd547fb";

    @Autowired
    private MockMvc mockMvc;

    private Round round;

    private ObjectId objectId;

    @BeforeEach
    public void setUp() {
        initMocks(this);
        round = Mocks.roundOne();
        objectId = Mocks.objectId();
    }

    @Test
    public void shouldGetAllRoundsByUserId() throws Exception {

        // setup
        given(userRoundService.getAllRoundsByUserId(STUB_USER_ID)).willReturn(
                Collections.singletonList(round));

        // mock the rounds/userId request
        RequestBuilder requestBuilder = Requests.getAllRoundsByUserId(STUB_USER_ID);

        // perform the requests
        MockHttpServletResponse response = mockMvc.perform(requestBuilder)
                .andReturn()
                .getResponse();

        // asserts
        assertNotNull(response);
        assertEquals(HttpStatus.OK.value(), response.getStatus());
    }

    //other tests
}

请求类(上面使用)

 public class Requests {

    private Requests() {}

    public static RequestBuilder getAllRoundsByUserId(String userId) {
        return MockMvcRequestBuilders
                .get("/users/" + userId + "/rounds/")
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON);
    }
}

Spring 安全配置

/**
 * Configures our application with Spring Security to restrict access to our API endpoints.
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${auth0.audience}")
    private String audience;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Override
    public void configure(HttpSecurity http) throws Exception {
            /*
            This is where we configure the security required for our endpoints and setup our app to serve as
            an OAuth2 Resource Server, using JWT validation.
            */

        http.cors().and().csrf().disable().sessionManagement().
                sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .mvcMatchers(HttpMethod.GET, "/users/**").authenticated()
                .mvcMatchers(HttpMethod.POST, "/users/**").authenticated()
                .mvcMatchers(HttpMethod.DELETE, "/users/**").authenticated()
                .mvcMatchers(HttpMethod.PUT, "/users/**").authenticated()
                .and()
                .oauth2ResourceServer().jwt();
    }

    @Bean
    JwtDecoder jwtDecoder() {
            /*
            By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
            indeed intended for our app. Adding our own validator is easy to do:
            */

        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                JwtDecoders.fromOidcIssuerLocation(issuer);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer,
                audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

抽象单元测试类

@ExtendWith(SpringExtension.class)
@SpringBootTest(
        classes = PokerStatApplication.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public abstract class AbstractUnitTests {
    // mock objects etc
}

原文由 java12399900 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 958
2 个回答

如果我正确理解您的情况,那么有一种解决方案。

在大多数情况下,如果令牌存在于请求标头中, JwtDecoder bean 执行令牌解析和验证。

您的配置示例:

     @Bean
    JwtDecoder jwtDecoder() {
        /*
        By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
        indeed intended for our app. Adding our own validator is easy to do:
        */

        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
            JwtDecoders.fromOidcIssuerLocation(issuer);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }

所以对于测试,你需要添加这个 bean 的存根,并且为了在 spring 上下文中替换这个 bean,你需要测试配置。

可能是这样的:

 @TestConfiguration
public class TestSecurityConfig {

  static final String AUTH0_TOKEN = "token";
  static final String SUB = "sub";
  static final String AUTH0ID = "sms|12345678";

  @Bean
  public JwtDecoder jwtDecoder() {
    // This anonymous class needs for the possibility of using SpyBean in test methods
    // Lambda cannot be a spy with spring @SpyBean annotation
    return new JwtDecoder() {
      @Override
      public Jwt decode(String token) {
        return jwt();
      }
    };
  }

  public Jwt jwt() {

    // This is a place to add general and maybe custom claims which should be available after parsing token in the live system
    Map<String, Object> claims = Map.of(
        SUB, USER_AUTH0ID
    );

    //This is an object that represents contents of jwt token after parsing
    return new Jwt(
        AUTH0_TOKEN,
        Instant.now(),
        Instant.now().plusSeconds(30),
        Map.of("alg", "none"),
        claims
    );
  }

}

要在测试中使用此配置,只需选择此测试安全配置:

@SpringBootTest(classes = TestSecurityConfig.class)

同样在测试请求中应该是带有标记的授权标头 Bearer .. something

以下是有关您的配置的示例:

     public static RequestBuilder getAllRoundsByUserId(String userId) {

        return MockMvcRequestBuilders
            .get("/users/" + userId + "/rounds/")
            .accept(MediaType.APPLICATION_JSON)
            .header(HttpHeaders.AUTHORIZATION, "Bearer token"))
            .contentType(MediaType.APPLICATION_JSON);
    }

原文由 Danil Kuznetsov 发布,翻译遵循 CC BY-SA 4.0 许可协议

对我来说,我让它变得非常简单。

我不想实际检查 JWT 令牌,这也可以被模拟。

看看这个安全配置。

 @Override
    public void configure(HttpSecurity http) throws Exception {

        //@formatter:off
        http
            .cors()
            .and()

            .authorizeRequests()
                .antMatchers("/api/v1/orders/**")
                .authenticated()
            .and()
            .authorizeRequests()
                .anyRequest()
                .denyAll()
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

            .and()
            .oauth2ResourceServer()
            .jwt();

然后在我的测试中,我使用了两件事

  • 为 jwtDecoder 提供模拟 bean
  • 使用 SecurityMockMvcRequestPostProcessors 模拟请求中的 JWT。这在以下依赖项中可用
         <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

这是它是如何完成的。

 @SpringBootTest
@AutoConfigureMockMvc
public class OrderApiControllerIT {

    @Autowired
    protected MockMvc mockMvc;

    @MockBean
    private JwtDecoder jwtDecoder;

    @Test
    void testEndpoint() {

     MvcResult mvcResult = mockMvc.perform(post("/api/v1/orders")
                .with(SecurityMockMvcRequestPostProcessors.jwt())
                .content(jsonString)
                .contentType(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().is2xxSuccessful())
            .andReturn();

}

就是这样,它应该可以工作。

原文由 Amrut Prabhu 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题