You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于JWT与PreAuthorized的Spring Boot OAuth集成测试问题

Spring Security OAuth2 ResourceServer JWT 集成方案

现有背景

当前基于Spring Security OAuth2 ResourceServer的JWT认证已正常运行,安全过滤器链配置如下:

httpSecurity.authorizeRequests()
            .anyRequest().authenticated()
            .and().cors()
            .and().csrf()
            .and().oauth2ResourceServer().jwt();

需引入自定义UserDetailsService结合@PreAuthorized实现角色权限控制,同时解决两个核心问题:


问题1:集成测试模拟JWT认证通过

方案:模拟JwtDecoder并携带认证头

在测试环境中,通过模拟JwtDecoder返回自定义JWT信息,让请求携带伪造的Bearer Token完成认证。

修改后的集成测试代码:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class ControllerTest {

    @LocalServerPort
    private int serverPort;

    @Autowired
    private TestRestTemplate testRestTemplate;

    // 测试环境模拟JwtDecoder,返回包含自定义用户名的JWT
    @Bean
    @Profile("test")
    public JwtDecoder jwtDecoder() {
        return token -> Jwt.withTokenValue("mock-token")
                .header("alg", "none")
                .claim("custom_user_name", "admin-user") // 对应自定义用户名字段
                .build();
    }

    @Test
    public void entitlementTest() {
        String uri = UriComponentsBuilder.fromHttpUrl("http://localhost")
                .port(serverPort)
                .path("/custom")
                .build()
                .toUriString();

        // 构造携带Bearer Token的请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth("mock-token");
        HttpEntity<Void> requestEntity = new HttpEntity<>(null, headers);

        ResponseEntity<String> response = testRestTemplate.exchange(uri, HttpMethod.GET, requestEntity, String.class);
        assertEquals(HttpStatus.OK.value(), response.getStatusCodeValue());
    }
}

备选方案:使用Spring Security Test注解

引入spring-security-test依赖后,可直接用@WithMockJwt注解快速模拟JWT认证:

@Test
@WithMockJwt(claims = @Claim(name = "custom_user_name", value = "admin-user"))
public void entitlementTest() {
    String uri = UriComponentsBuilder.fromHttpUrl("http://localhost")
            .port(serverPort)
            .path("/custom")
            .build()
            .toUriString();
    ResponseEntity<String> response = testRestTemplate.getForEntity(uri, String.class);
    assertEquals(HttpStatus.OK.value(), response.getStatusCodeValue());
}

问题2:指定JWT自定义用户名字段并结合UserDetailsService

步骤1:自定义JWT认证转换器

重写JwtAuthenticationConverter,从JWT的custom_user_name字段提取用户名:

@Component
public class CustomJwtAuthenticationConverter extends JwtAuthenticationConverter {
    @Override
    protected String extractUsername(Jwt jwt) {
        // 从自定义claim中获取用户名
        return jwt.getClaimAsString("custom_user_name");
    }
}

步骤2:配置JWT认证提供者绑定UserDetailsService

创建JwtAuthenticationProvider,关联自定义UserDetailsService以加载用户角色权限:

@Bean
public JwtAuthenticationProvider jwtAuthenticationProvider(JwtDecoder jwtDecoder, UserDetailsService userDetailsService) {
    JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwtDecoder);
    // 绑定UserDetailsService,加载用户权限
    provider.setUserDetailsContextMapper(userDetails -> {
        UserDetails user = userDetailsService.loadUserByUsername(userDetails.getUsername());
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                "", // JWT已完成身份认证,密码无需校验
                user.getAuthorities()
        );
    });
    return provider;
}

步骤3:更新安全过滤器链配置

将自定义转换器和认证提供者注入到HttpSecurity配置中,并开启方法级权限控制:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启@PreAuthorize注解支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomJwtAuthenticationConverter customJwtAuthenticationConverter;
    private final JwtAuthenticationProvider jwtAuthenticationProvider;

    public SecurityConfig(CustomJwtAuthenticationConverter customJwtAuthenticationConverter,
                          JwtAuthenticationProvider jwtAuthenticationProvider) {
        this.customJwtAuthenticationConverter = customJwtAuthenticationConverter;
        this.jwtAuthenticationProvider = jwtAuthenticationProvider;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .cors()
                .and()
                .csrf().disable() // 生产环境按需配置
                .oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(customJwtAuthenticationConverter)
                .and()
                .authenticationProvider(jwtAuthenticationProvider);
    }
}

步骤4:完善CustomUserDetailsService实现

调用微服务获取用户角色并构建UserDetails对象(注意ROLE_前缀匹配@PreAuthorize的hasRole规则):

public class CustomUserDetailsService implements UserDetailsService {

    private final IIUserService iiUserService;

    public CustomUserDetailsService(IIUserService iiUserService) {
        this.iiUserService = iiUserService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        IIUser user = iiUserService.getUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在:" + username);
        }
        // 转换为Spring Security权限对象,自动添加ROLE_前缀
        List<GrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                .collect(Collectors.toList());
        return new User(user.getUsername(), "", authorities);
    }
}

内容的提问来源于stack exchange,提问作者daniel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 22:15:01