怎么测试 Spring Security 保护的接口?@WithMockUser 等注解怎么用?
简化版
**测试受 Spring Security 保护的接口有个难题:接口需要「已登录 + 有权限」才能访问,测试时怎么「模拟一个已认证的用户」?答案是 spring-security-test 提供的一套测试工具。**核心工具:① @WithMockUser——在测试方法上标注,模拟一个已认证的用户(可指定用户名、角色、权限),如 @WithMockUser(roles = "ADMIN") 就是「模拟一个 ADMIN 角色的已登录用户」来测;② @WithAnonymousUser——模拟未登录(匿名)用户;③ @WithUserDetails——用真实的 UserDetailsService 加载一个用户(比 MockUser 更真实);④ MockMvc 的 with() 后置处理器——.with(user("admin").roles("ADMIN"))、.with(csrf())(带 CSRF token)等,在请求级别模拟。典型用法:用 @WithMockUser 模拟不同角色的用户,验证「有权限的能访问(200)、没权限的被拒(403)、没登录的(401)」。关键点:这些工具把认证信息设置进 SecurityContext,让被测接口以为「有个已登录用户」,从而能测试授权逻辑,而不用真正走登录流程。
详细版
核心测试工具:
| 工具 | 作用 |
|---|---|
| @WithMockUser | 模拟一个已认证用户(指定用户名/角色/权限) |
| @WithAnonymousUser | 模拟匿名(未登录)用户 |
| @WithUserDetails | 用真实 UserDetailsService 加载用户 |
| @WithSecurityContext | 自定义 SecurityContext(灵活) |
| user(…) 后置处理器 | MockMvc 请求级模拟用户 |
| csrf() 后置处理器 | 带 CSRF token |
// 引入 spring-security-test 依赖
@SpringBootTest
@AutoConfigureMockMvc
class SecurityTest {
@Autowired MockMvc mockMvc;
// ① 模拟 ADMIN 用户访问 /admin → 应该 200
@Test
@WithMockUser(roles = "ADMIN")
void adminCanAccessAdmin() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isOk());
}
// ② 模拟普通用户访问 /admin → 应该 403(无权限)
@Test
@WithMockUser(roles = "USER")
void userCannotAccessAdmin() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isForbidden()); // 403
}
// ③ 未登录访问受保护接口 → 401
@Test
void anonymousGets401() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isUnauthorized()); // 401
}
// ④ MockMvc 后置处理器:请求级模拟 + CSRF
@Test
void postWithUserAndCsrf() throws Exception {
mockMvc.perform(post("/api/data")
.with(user("admin").roles("ADMIN")) // 模拟用户
.with(csrf())) // 带 CSRF token
.andExpect(status().isOk());
}
}
⚠️ 测试 Spring Security 有两个常见的坑:CSRF 和「注解 vs 请求级」的选择。CSRF 坑:Spring Security 默认对 POST/PUT/DELETE 等「改数据」的请求做 CSRF 防护,测试这些请求时如果不带 CSRF token,会被拒绝(403)——所以要用
.with(csrf())后置处理器带上 CSRF token(除非你的接口关闭了 CSRF,如纯 JWT 的 REST API)。很多人测 POST 接口失败,就是忘了csrf()。注解 vs 请求级的选择:@WithMockUser(方法注解)作用于整个测试方法(简洁、常用,适合「这个测试用这个用户」);.with(user(...))(MockMvc 后置处理器)作用于单个请求(灵活,适合「同一个测试里不同请求用不同用户」)。另外@WithMockUser是「凭空造」一个用户(不查数据库),@WithUserDetails才是「用真实 UserDetailsService 加载真实用户」——需要更真实的测试(如验证用户的真实权限)时用@WithUserDetails。
完整版教学
一、问题:怎么模拟已认证用户
先理解测试受保护接口的难题:
受 Spring Security 保护的接口:
需要"已登录 + 有权限"才能访问
没登录 → 401,没权限 → 403
测试的难题:
测试时怎么访问这些接口?
① 真的走登录流程?—— 麻烦(要构造登录请求、拿 token/session)
② 关掉安全再测?—— 测不到安全逻辑(授权规则)
想要的:
能"模拟一个已认证的用户"(指定角色/权限)
→ 让被测接口以为"有个已登录用户"
→ 从而测试授权逻辑(有权限的能访问、没权限的被拒)
→ 不用真正走登录流程
解决:spring-security-test 提供测试工具
@WithMockUser、@WithUserDetails、MockMvc 后置处理器
→ 把认证信息设置进 SecurityContext
→ 被测接口拿到这个认证信息 → 以为用户已登录
所以测试的核心:模拟认证,测授权
测试受保护接口的难题是「怎么模拟已认证用户」——接口需要「已登录+有权限」,测试时真走登录流程麻烦、关掉安全又测不到授权逻辑。想要的是「模拟一个已认证用户(指定角色/权限),让被测接口以为有个已登录用户,从而测授权逻辑,不用真正登录」。解决:spring-security-test 提供工具(@WithMockUser 等)把认证信息设置进 SecurityContext。理解「测试难题:怎么模拟已认证用户(真登录麻烦/关安全测不到授权);想要模拟已认证用户测授权逻辑不用真登录;spring-security-test 把认证信息设进 SecurityContext」,就理解了测试的核心问题。
二、@WithMockUser:模拟用户
@WithMockUser 是最常用的——模拟一个用户:
@WithMockUser:在测试方法(或类)上标注,模拟一个已认证用户
@WithMockUser // 默认:用户名 user,角色 ROLE_USER,密码 password
@WithMockUser(username = "admin", roles = {"ADMIN", "USER"})
@WithMockUser(authorities = {"user:read", "user:write"}) // 指定权限
参数:
username:用户名(默认 "user")
roles:角色(自动加 ROLE_ 前缀,roles="ADMIN" → ROLE_ADMIN)
authorities:权限(精确,不加前缀)
password:密码
作用范围:
标在方法上 → 这个测试方法用这个用户
标在类上 → 类里所有测试方法用这个用户
原理:
测试执行前,@WithMockUser 构造一个 Authentication
放进 SecurityContext
→ 被测代码从 SecurityContext 拿到这个"用户"
→ 以为已登录(有指定的角色/权限)
特点:
"凭空造"一个用户——不查数据库、不走 UserDetailsService
→ 快、简单,适合测授权逻辑(有没有某角色/权限)
注意 roles vs authorities:
roles = "ADMIN" → 自动加前缀 → ROLE_ADMIN(配合 hasRole)
authorities = "ROLE_ADMIN" → 精确(配合 hasAuthority)
→ 别混,和 hasRole/hasAuthority 的前缀规则一致
@WithMockUser 是最常用的——模拟一个已认证用户(@WithMockUser(roles = "ADMIN") 或 authorities = {...})。参数:username、roles(自动加 ROLE_ 前缀)、authorities(精确不加前缀)。作用范围:标方法(该方法用这用户)或标类(所有方法)。原理:测试前构造 Authentication 放进 SecurityContext、被测代码拿到以为已登录。特点:凭空造用户(不查数据库、不走 UserDetailsService),快、简单。注意 roles 自动加前缀(配 hasRole)、authorities 精确(配 hasAuthority),和前缀规则一致。理解「@WithMockUser 模拟已认证用户(roles/authorities)、标方法或类、原理是构造 Authentication 放进 SecurityContext、凭空造用户不查库快;roles 自动加前缀 authorities 精确」,就掌握了 @WithMockUser。
三、@WithUserDetails:真实用户
@WithUserDetails 用真实的 UserDetailsService 加载用户——更真实:
@WithUserDetails:用真实的 UserDetailsService 加载一个用户
@WithUserDetails("admin@example.com")
→ 调 UserDetailsService.loadUserByUsername("admin@example.com")
→ 加载真实用户(从数据库/真实数据源)
→ 用这个真实用户的真实角色/权限来测试
对比 @WithMockUser:
@WithMockUser:凭空造用户(你指定角色/权限,不查数据源)
→ 快、简单,但用户是"假的"
@WithUserDetails:真实加载用户(走 UserDetailsService)
→ 更真实(用户的真实权限、真实数据)
→ 但要有真实的 UserDetailsService 和用户数据(如测试数据)
什么时候用哪个:
① 只测授权规则(有 ADMIN 角色能不能访问)→ @WithMockUser(简单)
② 需要真实用户的完整信息、真实权限 → @WithUserDetails
③ 测试涉及用户的具体数据(如"只能看自己的数据")→ @WithUserDetails
配合测试数据:
@WithUserDetails 需要用户真实存在(UserDetailsService 能加载到)
→ 测试里要准备测试用户数据(@Sql 插入、或 mock UserDetailsService)
所以 @WithUserDetails = 更真实的用户模拟(走真实加载流程)
@WithUserDetails 用真实的 UserDetailsService 加载用户(@WithUserDetails("admin@example.com") 调 loadUserByUsername 加载真实用户、用真实角色/权限测)。对比 @WithMockUser:MockUser 凭空造(快简单但假)、WithUserDetails 真实加载(更真实但要真实数据)。选择:只测授权规则用 MockUser、需要真实用户完整信息/真实权限/涉及用户具体数据用 WithUserDetails。WithUserDetails 需要用户真实存在(准备测试数据)。理解「@WithUserDetails 用真实 UserDetailsService 加载用户(真实角色权限);对比 MockUser 凭空造(快简单假)vs WithUserDetails 真实加载(真实但要数据);只测授权用 MockUser、需要真实用户用 WithUserDetails」,就掌握了 @WithUserDetails。
四、MockMvc 后置处理器
MockMvc 的 with() 后置处理器——请求级模拟:
MockMvc 后置处理器(RequestPostProcessor):
在单个请求上模拟安全上下文(比注解更灵活)
user(...) 后置处理器(模拟用户,请求级):
mockMvc.perform(get("/admin")
.with(user("admin").roles("ADMIN")))
→ 这个请求模拟 admin 用户
→ 和 @WithMockUser 类似,但作用于"单个请求"而非整个方法
csrf() 后置处理器(带 CSRF token):
mockMvc.perform(post("/api/data")
.with(csrf())) // 带上有效的 CSRF token
→ 测试 POST/PUT/DELETE 等需要 CSRF 的请求
httpBasic(...) 后置处理器(HTTP Basic 认证):
.with(httpBasic("user", "password"))
jwt() / oauth2Login() 等(OAuth2/JWT 场景)
注解 vs 后置处理器的选择:
@WithMockUser(注解):
作用整个测试方法,简洁,"这个测试用这个用户"
.with(user(...))(后置处理器):
作用单个请求,灵活,"同一个测试里不同请求用不同用户"
组合使用:
.with(user("admin").roles("ADMIN")).with(csrf())
→ 模拟用户 + 带 CSRF(测 POST 接口常这样)
MockMvc 后置处理器(with())——请求级模拟(比注解灵活):user(...)(模拟用户、.with(user("admin").roles("ADMIN"))、作用单个请求)、csrf()(带 CSRF token、测 POST/PUT/DELETE)、httpBasic(...)、jwt()。注解 vs 后置处理器:@WithMockUser(注解、作用整个方法、简洁)、.with(user(...))(后置处理器、作用单请求、灵活,同测试里不同请求用不同用户)。组合:.with(user(...)).with(csrf())(模拟用户+带 CSRF,测 POST 常这样)。理解「MockMvc 后置处理器 with():user(模拟用户请求级)/csrf(带 CSRF token 测 POST)/httpBasic;注解 vs 后置处理器:@WithMockUser 作用整个方法简洁 vs .with(user)作用单请求灵活;组合 user+csrf 测 POST」,就掌握了 MockMvc 后置处理器。
五、CSRF 坑
CSRF 是测试 Spring Security 的常见坑:
CSRF 防护:
Spring Security 默认对"改数据"的请求(POST/PUT/DELETE/PATCH)
做 CSRF 防护——要求请求带有效的 CSRF token
没带 → 拒绝(403)
测试时的坑:
测试 POST 接口,如果不带 CSRF token → 被拒绝(403)
→ 很多人测 POST 失败就是忘了 CSRF
解决:.with(csrf())
mockMvc.perform(post("/api/data").with(csrf()))
→ 带上有效的 CSRF token → 通过
什么时候不用 csrf():
如果接口关闭了 CSRF 防护(如纯 JWT 的 REST API 常关 CSRF)
→ http.csrf(csrf -> csrf.disable())
→ 测试就不用 .with(csrf())
判断要不要 csrf():
看被测接口有没有 CSRF 防护
有(默认,表单/Session 场景)→ 测 POST 要 .with(csrf())
没有(JWT REST API 常关)→ 不用
所以:测 POST/PUT/DELETE 接口,注意 CSRF
默认要带 .with(csrf()),除非接口关了 CSRF
CSRF 坑:Spring Security 默认对「改数据」的请求(POST/PUT/DELETE/PATCH)做 CSRF 防护、要求带有效 CSRF token、没带拒绝(403)。测试时测 POST 接口不带 CSRF token 会被拒(403)(很多人测 POST 失败就是忘了 CSRF)。解决:.with(csrf()) 带上 token。什么时候不用:接口关闭了 CSRF(纯 JWT REST API 常关 http.csrf().disable())就不用。判断看被测接口有没有 CSRF 防护。理解「CSRF 坑:默认对 POST/PUT/DELETE 做 CSRF 防护、测这些接口不带 token 被拒 403;解决.with(csrf())带 token;JWT REST API 常关 CSRF 就不用;测 POST 注意 CSRF」,就避开了 CSRF 坑。
六、实践与总结
总结安全测试的实践:
实践步骤:
① 引入 spring-security-test 依赖
② @SpringBootTest + @AutoConfigureMockMvc(或 @WebMvcTest)
③ 用 @WithMockUser 模拟用户,测授权规则
④ 测 POST/PUT/DELETE 记得 .with(csrf())
⑤ 验证:有权限 200、无权限 403、未登录 401
典型测试用例:
① @WithMockUser(roles="ADMIN") 访问 /admin → 200
② @WithMockUser(roles="USER") 访问 /admin → 403
③ 无注解(匿名)访问受保护接口 → 401
④ 测方法级权限(@PreAuthorize):不同角色调用,验证放行/拒绝
工具选择:
简单模拟用户、整个测试 → @WithMockUser
真实用户、真实权限 → @WithUserDetails
单请求模拟、灵活 → .with(user(...))
测改数据接口 → 加 .with(csrf())
注意点:
① roles 自动加 ROLE_ 前缀、authorities 精确(和 hasRole/hasAuthority 一致)
② 测 POST 记得 csrf()
③ @WithMockUser 是假用户(不查库)、@WithUserDetails 是真用户
④ 验证 401(未认证)vs 403(无权限)的区别
核心总结:
spring-security-test 提供模拟认证的工具
@WithMockUser 模拟用户、@WithUserDetails 真实加载
MockMvc .with(user/csrf) 请求级
测授权:有权限 200、无权限 403、未登录 401;改数据接口带 csrf()
安全测试实践:① 引入 spring-security-test、② @SpringBootTest+@AutoConfigureMockMvc、③ @WithMockUser 模拟用户测授权、④ 测 POST 记得 .with(csrf())、⑤ 验证有权限 200/无权限 403/未登录 401。工具选择:简单模拟用 @WithMockUser、真实用户用 @WithUserDetails、单请求用 .with(user(...))、改数据接口加 .with(csrf())。注意:roles 加前缀 authorities 精确、测 POST 带 csrf、MockUser 假 WithUserDetails 真、区分 401/403。理解「实践:引入依赖+@WithMockUser 测授权+测 POST 加 csrf+验证 200/403/401;工具选择:MockUser 简单/WithUserDetails 真实/with(user)单请求/改数据加 csrf」,就掌握了安全测试的实践。
记忆钩子:「测试 Spring Security 保护的接口用 spring-security-test:①@WithMockUser 模拟已认证用户(roles 自动加 ROLE_ 前缀/authorities 精确,凭空造不查库,标方法或类,原理是构造 Authentication 放进 SecurityContext)②@WithAnonymousUser 模拟未登录③@WithUserDetails 用真实 UserDetailsService 加载真实用户(更真实但要数据)④MockMvc 后置处理器.with(user(…))请求级模拟/.with(csrf())带 CSRF token;★CSRF 坑:默认对 POST/PUT/DELETE 做 CSRF 防护、测这些接口不带 token 被拒 403、要.with(csrf())(JWT REST API 关 CSRF 就不用);典型:有权限 200、无权限 403、未登录 401;注解作用整个方法 vs 后置处理器作用单请求」。
七、常见误区与追问
- 误区:测试受保护接口要真的走登录流程。 不用——spring-security-test 提供 @WithMockUser 等工具,直接模拟一个已认证用户(把认证信息设置进 SecurityContext),让被测接口以为有个已登录用户,从而测试授权逻辑,不用构造登录请求、拿 token/session。
- 误区:测 POST 接口不用管 CSRF。 要管——Spring Security 默认对 POST/PUT/DELETE 等改数据的请求做 CSRF 防护,测试时不带 CSRF token 会被拒(403);要用 .with(csrf()) 带上 token;除非接口关闭了 CSRF(如纯 JWT 的 REST API);很多人测 POST 失败就是忘了 csrf()。
- 误区:@WithMockUser 会查数据库加载真实用户。 不会——@WithMockUser 是「凭空造」一个用户(你指定用户名/角色/权限,不查数据源、不走 UserDetailsService),快、简单,适合测授权规则;要用真实用户(走 UserDetailsService 加载)用 @WithUserDetails。
- 误区:@WithMockUser(roles=“ROLE_ADMIN”) 是对的。 错——roles 参数会自动加 ROLE_ 前缀,roles=“ADMIN” 就是 ROLE_ADMIN;写 roles=“ROLE_ADMIN” 会变成 ROLE_ROLE_ADMIN(双前缀);要精确指定带前缀的权限用 authorities=“ROLE_ADMIN”;这和 hasRole/hasAuthority 的前缀规则一致。
- 追问:@WithMockUser 和 @WithUserDetails 有什么区别? @WithMockUser「凭空造」一个用户(你指定用户名、角色、权限,不查数据库、不走 UserDetailsService),快、简单,适合只测授权规则(有没有某角色/权限);@WithUserDetails 用真实的 UserDetailsService 加载真实用户(loadUserByUsername,从数据源加载真实的角色/权限),更真实,适合需要用户完整真实信息、或测试涉及用户具体数据(如「只能看自己的数据」)的场景,但要准备真实的测试用户数据。
- 追问:测试 Spring Security 的 POST 接口为什么老是 403? 大概率是忘了 CSRF token——Spring Security 默认对 POST/PUT/DELETE/PATCH 等改数据的请求做 CSRF 防护,要求带有效的 CSRF token,测试时不带就会被拒绝(403);解决办法是在 MockMvc 请求上加 .with(csrf());如果你的接口关闭了 CSRF(如纯 JWT 的 REST API 用 http.csrf().disable()),那测试就不用带 csrf()。
- 追问:怎么验证「有权限能访问、没权限被拒、没登录也被拒」? 用不同的模拟用户 + 验证不同状态码:① @WithMockUser(roles=“ADMIN”) 访问需要 ADMIN 的接口 → andExpect(status().isOk())(200,有权限);② @WithMockUser(roles=“USER”) 访问同接口 → andExpect(status().isForbidden())(403,无权限);③ 不加任何 @WithMockUser(匿名/未登录)访问受保护接口 → andExpect(status().isUnauthorized())(401,未认证);这样覆盖了授权的三种情况。
八、加强记忆
测试受 Spring Security 保护的接口的难题是「怎么模拟已认证用户」——用 spring-security-test 提供的工具。核心工具:① @WithMockUser——标在测试方法/类上模拟一个已认证用户(roles(自动加 ROLE_ 前缀)/authorities(精确),凭空造用户不查数据库,原理是构造 Authentication 放进 SecurityContext,快、简单,适合测授权规则);② @WithAnonymousUser(模拟未登录);③ @WithUserDetails(用真实 UserDetailsService 加载真实用户,更真实但要准备数据);④ MockMvc 后置处理器——.with(user("admin").roles("ADMIN"))(请求级模拟用户)、.with(csrf())(带 CSRF token)。CSRF 坑:Spring Security 默认对 POST/PUT/DELETE 做 CSRF 防护,测这些接口不带 token 会被拒(403),要 .with(csrf())(JWT REST API 关 CSRF 就不用)。典型验证:有权限 200、无权限 403、未登录 401。注解 vs 后置处理器:@WithMockUser 作用整个测试方法(简洁)、.with(user(...)) 作用单个请求(灵活);@WithMockUser 是假用户、@WithUserDetails 是真用户。一句话「测 Spring Security 接口用 spring-security-test:@WithMockUser 模拟已认证用户(roles 加前缀/authorities 精确,凭空造不查库)、@WithUserDetails 真实加载、MockMvc .with(user/csrf)请求级;★CSRF 坑:测 POST/PUT/DELETE 要.with(csrf())否则 403;验证有权限 200/无权限 403/未登录 401」。