为何@WithMockUser配置正确,RestAssured测试仍失败?
我来帮你拆解这个问题——你遇到的核心痛点其实是线程隔离导致的SecurityContext无法传递,咱们一步步说清楚:
先还原你的场景
你有一个带方法级安全的控制器:
@RestController public class DummyController { @GetMapping("/") @PreAuthorize("hasRole('TEST')") public String test() { return "hello"; } }
宽松的安全配置(允许所有请求,但开启方法级安全):
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().permitAll(); } }
还有失败的RestAssured测试:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @RunWith(SpringRunner.class) public class DummyControllerITest { @LocalServerPort private int port; @Test @WithMockUser(roles = "TEST") public void name() throws Exception { RestAssured.given() .port(port) .when() .get("/") .then() .statusCode(HttpStatus.OK.value()); } }
为什么测试会失败?
你调试发现的现象已经很关键了:测试线程有SecurityContext,但请求处理线程没有。
原因很简单:
@WithMockUser的作用是把模拟用户信息绑定到当前测试线程的SecurityContext里,它只在这个线程内有效。- 而RestAssured是通过发送真实的HTTP请求到Spring Boot的嵌入式服务器,服务器会用另一个独立的线程来处理这个请求。两个线程的
SecurityContext是完全隔离的,所以处理请求的线程根本拿不到你在测试线程里设置的模拟用户。
换句话说,@WithMockUser是给同线程的代码用的(比如直接调用Service层方法测试),但跨线程的HTTP请求(比如RestAssured、Postman这类外部调用)根本吃不到这个注解的红利。
怎么解决?
核心思路是:让身份信息通过HTTP请求本身传递给服务器,让服务器在处理请求的线程中重新构建合法的SecurityContext。最直接的方案是用Spring Security的HTTP认证机制,比如Basic认证,下面给你完整的实现:
1. 修改SecurityConfig,开启Basic认证并配置测试用户
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().permitAll() .and() .httpBasic(); // 开启Basic认证支持 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 配置内存测试用户,角色为TEST auth.inMemoryAuthentication() .withUser("test-user") .password("{noop}test-pass") // {noop}表示不加密,仅测试用 .roles("TEST"); } }
2. 修改RestAssured测试,添加Basic认证信息
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @RunWith(SpringRunner.class) public class DummyControllerITest { @LocalServerPort private int port; @Test public void testWithValidRole() throws Exception { RestAssured.given() .port(port) .auth().basic("test-user", "test-pass") // 携带Basic认证信息 .when() .get("/") .then() .statusCode(HttpStatus.OK.value()); } }
这样修改后,测试就能正常通过了——因为RestAssured会把用户名密码通过Authorization请求头发送给服务器,Spring Security的BasicAuthenticationFilter会解析这个请求头,验证用户身份,然后把正确的SecurityContext设置到请求处理线程中,@PreAuthorize("hasRole('TEST')")就能正确识别用户角色了。
其他可选方案
如果你不想用Basic认证,也可以:
- 用JWT令牌:生成测试用的JWT,在请求头里携带
Authorization: Bearer <token> - 自定义RestAssured过滤器:把测试线程的SecurityContext序列化后放到请求头,再写一个服务器端过滤器解析并设置到请求线程(这个相对复杂,一般测试用Basic足够)
另外,如果你愿意替换RestAssured为Spring官方的TestRestTemplate,它可以和@WithMockUser配合使用——因为TestRestTemplate是Spring内部的客户端,会自动把测试线程的SecurityContext传递到请求处理线程,不过这属于替换工具的方案了。
内容的提问来源于stack exchange,提问作者user1978011

