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

jav*_*900 13 java spring unit-testing jwt spring-boot

我已经按照这个例子在我的 Spring Boot REST API 中添加了JWT身份验证。Auth0

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

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

单元测试类:

    @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
}
Run Code Online (Sandbox Code Playgroud)

请求类(上面使用过):

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

}
Run Code Online (Sandbox Code Playgroud)

春季安全配置:

/**
 * 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

抽象单元测试类:

@ExtendWith(SpringExtension.class)
@SpringBootTest(
    classes = PokerStatApplication.class,
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public abstract class AbstractUnitTests {

   // mock objects etc


}
Run Code Online (Sandbox Code Playgroud)

Dan*_*sov 9

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

在大多数情况下,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;
    }
Run Code Online (Sandbox Code Playgroud)

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

它可能是这样的:

@TestConfiguration
public class TestSecurityConfig {

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

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

}
Run Code Online (Sandbox Code Playgroud)

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

@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);
    }
Run Code Online (Sandbox Code Playgroud)

  • 我认为您在 TestSecurityConfig 中的 JwtDecoder 上缺少“@Bean”。至少我通过在真正的 JwtDecoder bean 定义上添加它加上 `@MissingOnConditionalBean` 来让它工作。 (2认同)

Amr*_*bhu 9

对我来说,我把它做得非常简单。

我不想实际检查 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();

Run Code Online (Sandbox Code Playgroud)

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

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

这是它是如何完成的。

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

}

Run Code Online (Sandbox Code Playgroud)

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