返回

Java Spring Mock Bearer Token 认证与 OAuth2 集成测试方案

java

Java Spring 集成测试中 Mock Bearer Token 认证与 OAuth2 的问题及解决方案

在 Java Spring 应用中,对 Web 层进行集成测试时,经常会遇到模拟 Bearer Token 认证的场景。当控制器方法参数包含 BearerTokenAuthentication@AuthenticationPrincipal OidcUser 时,如何正确地 Mock 这些参数并保证测试的准确性,是一个常见的问题。本文将深入分析该问题,并提供多种解决方案。

问题分析

问题的核心在于 Spring Security 上下文中用户 Principal 类型与控制器期望的 BearerTokenAuthentication 类型不匹配。具体表现为测试过程中抛出 java.lang.IllegalStateException: Current user principal is not of type [org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication] 异常。

出现这个问题的原因可能包括:

  1. 自定义 JwtAuthenticationConverter 未生效: 虽然配置了自定义 JwtBearerTokenAuthenticationConverter,但测试过程中可能未被正确应用。
  2. Mock 方式不兼容: 使用 @WithJwt 注解进行 Mock 时,可能只生成了 JwtAuthenticationToken,而不是 BearerTokenAuthentication
  3. 测试配置缺失: Spring Boot 测试环境的配置可能与实际运行环境存在差异,导致认证机制行为不一致。

解决方案

针对上述原因,可以采用以下解决方案:

方案一:使用 SecurityContextHolder 手动设置 Authentication

直接构建 BearerTokenAuthentication 对象并设置到 Spring Security 上下文中。这种方式最为直接,可以精确控制认证对象。

代码示例:

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
// ... 其他导入

@Test
public void testGetBooks() throws Exception {

    // 构建 BearerTokenAuthentication 对象
    Jwt jwt = Jwt.withTokenValue("mock-token")
            .header("alg", "none")
            .claim("sub", "user123")
            .build();

    BearerTokenAuthentication authentication = new BearerTokenAuthentication(jwt, List.of(), jwt);
    SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
    securityContext.setAuthentication(authentication);

    SecurityContextHolder.setContext(securityContext);

    ResponseEntity<List<Book>> responseEntity = ResponseEntity.ok(List.of(sampleBooks));
    when(feignClient.getBooks()).thenReturn(responseEntity);

    this.mockMvc.perform(MockMvcRequestBuilders.get("/books")
                    .with(SecurityMockMvcRequestPostProcessors.csrf()))
            .andExpect(status().isOk());
}

操作步骤:

  1. 在测试方法中,使用 Jwt.withTokenValue() 方法构建一个模拟 Jwt 对象。
  2. 使用 BearerTokenAuthentication 构造函数,传入 Jwt 对象、授权信息和凭据(通常也是 Jwt 对象),创建一个 BearerTokenAuthentication 实例。
  3. 创建空的 SecurityContext,并将构建好的 BearerTokenAuthentication 对象设置进去。
  4. SecurityContext 设置到 SecurityContextHolder 中。
  5. 执行 MockMvc 请求。

额外安全建议: 在生产环境中,应该使用安全的密钥和签名算法。 Mock JWT 仅用于测试目的,切勿在生产环境中使用 Mock JWT。

方案二:替换 JwtAuthenticationConverter 使用 Mockito 替换 JwtAuthenticationConverter 并指定返回自定义 BearerTokenAuthentication 对象。

代码示例:

import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
// ... 其他导入

@SpringBootTest
@AutoConfigureMockMvc
public class BookApiTest {
    // ... 其他成员变量

    @MockBean
    JwtAuthenticationConverter jwtAuthenticationConverter;

    // ... 其他方法

    @Test
    @WithJwt("jwt.json")
    public void testGetBooks() throws Exception {
         Jwt jwt = Jwt.withTokenValue("mock-token")
                 .header("alg", "none")
                 .claim("sub", "user123")
                 .build();

        BearerTokenAuthentication authentication = new BearerTokenAuthentication(jwt, List.of(), jwt);

        when(jwtAuthenticationConverter.convert(any(Jwt.class))).thenReturn(authentication);

        ResponseEntity<List<Book>> responseEntity = ResponseEntity.ok(List.of(sampleBooks));
        when(feignClient.getBooks()).thenReturn(responseEntity);

        this.mockMvc.perform(MockMvcRequestBuilders.get("/books")
                        .with(SecurityMockMvcRequestPostProcessors.csrf()))
                .andExpect(status().isOk());
    }
}

操作步骤:

  1. 使用 @MockBean 注解 Mock JwtAuthenticationConverter
  2. 在测试方法中,使用 Jwt.withTokenValue() 构建模拟 Jwt 对象。
  3. 构建 BearerTokenAuthentication 对象。
  4. 使用 Mockito.when 指定 JwtAuthenticationConverter.convert 方法的返回值,改为 Mock 的 BearerTokenAuthentication
  5. 执行 MockMvc 请求。

额外安全建议: 测试过程中使用的 JWT 模拟数据不应该包含敏感信息。

方案三: 使用 @Import 替换 security configuration 来引入专门用于测试的安全配置。该配置可以移除 OAuth2 的配置,只启用基本的认证。

代码示例:


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Import(BookApiTest.TestSecurityConfig.class)
public class BookApiTest {
    // ... 成员变量与之前相同

    @Configuration
    @EnableWebSecurity
     public static class TestSecurityConfig{
         @Bean
         @Primary
         public SecurityFilterChain securityFilterChainTest(HttpSecurity http) throws Exception {
             http.csrf(AbstractHttpConfigurer::disable)
                     .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll());
             return http.build();
         }
     }

     // ... 测试方法同方案二

    }

操作步骤:

  1. 创建一个测试安全配置 TestSecurityConfig
  2. @ActiveProfiles("test") 注解,使得 test 的配置仅在测试环境下启用.
  3. @Import 引入专门为测试准备的安全配置 TestSecurityConfig,覆盖原有安全配置.
  4. 移除了 Jwt decoder 及 Authentication convert 相关的代码. 这样 Spring security 上下文中便不会有这些组件. 可以测试 controller 本身的功能
  5. 在测试的安全配置中 http.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) 配置绕过了权限认证,以保证单元测试专注于测试业务逻辑.
    额外安全建议: 分离测试环境与生产环境的安全配置。 保证测试环境的配置简单化. 生产环境的配置复杂化及安全性最大化。

总结

本文针对 Java Spring 集成测试中 Mock Bearer Token 认证与 OAuth2 的问题,提出了三种解决方案。开发者可以根据具体情况选择最合适的方案。在选择方案时,需要综合考虑测试的精度、代码的复杂度和可维护性等因素。通过合理运用 Mock 技术,可以有效地提高集成测试的效率和准确性,确保应用的安全性和可靠性。

相关资源

希望以上解决方案能帮助您解决 Java Spring 集成测试中遇到的 Bearer Token 认证 Mock 问题。