如何在春季启动单元测试中模拟JWT真实性?

2022-09-02 04:03:48

此示例之后,我已将 身份验证 添加到我的 Spring Boot REST API 中。JWTAuth0

现在,正如预期的那样,我以前工作的单元测试给出了响应代码,而不是因为我没有在测试中传递任何JWT。Controller401 Unauthorized200 OK

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

单元测试类:

    @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);
    }

}

弹簧安全配置:

/**
 * 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


}

答案 1

如果我正确理解您的情况,则有一个解决方案。

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

配置中的示例:

    @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的存根,并且为了在春季上下文中替换此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);
    }

答案 2

对于像我这样的其他人,在从似乎大量的StackOverlow那里收集了关于如何做到这一点的信息之后,以下是最终对我有用的摘要(使用Kotlin语法,但它也适用于Java):

步骤 1 - 定义要在测试中使用的自定义 JWT 解码器

请注意该条目 - 这是最终可通过字段访问的用户名。JwtClaimNames.SUBauthentication.getName()

val jwtDecoder = JwtDecoder {
        Jwt(
                "token",
                Instant.now(),
                Instant.MAX,
                mapOf(
                        "alg" to "none"
                ),
                mapOf(
                        JwtClaimNames.SUB to "testUser"
                )
        )
}

步骤 2 - 定义测试配置

此类位于您的文件夹中。我们这样做是为了将实际实现替换为始终将用户视为经过身份验证的存根。test

请注意,我们尚未完成,请同时检查步骤3。

@TestConfiguration
class TestAppConfiguration {

    @Bean // important
    fun jwtDecoder() {
        // Initialize JWT decoder as described in step 1
        // ...

        return jwtDecoder
    }

}

步骤 3 - 更新主配置以避免 Bean 冲突

如果没有此更改,您的测试和生产 Bean 将发生冲突,从而导致冲突。添加此行会延迟 Bean 的分辨率,并让 Spring 将测试 Bean 置于生产 bean 之上。

但是,有一点需要注意,因为此更改有效地消除了 JwtDecoder 实例的生产版本中的 Bean 冲突保护。

@Configuration
class AppConfiguration {

    @Bean
    @ConditionalOnMissingBean // important
    fun jwtDecoder() {
        // Provide decoder as you would usually do
    }

}

步骤 4 - 在测试中导入测试应用程序配置

这可以确保您的测试实际上考虑了测试配置。

@SpringBootTest
@Import(TestAppConfiguration::class)
class MyTest {

    // Your tests

}

步骤 5 - 向测试添加@WithMockUser注释

您实际上不需要为批注提供任何参数。

@Test
@WithMockUser
fun myTest() {
    // Test body
}

步骤 6 - 在测试期间提供身份验证标头

mockMvc
    .perform(
        post("/endpointUnderTest")
            .header(HttpHeaders.AUTHORIZATION, "Bearer token") // important
    )
    .andExpect(status().isOk)

推荐