基于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
相关产品推荐
相关产品推荐

