← 返回题目列表

怎么测试 Spring Security 保护的接口?@WithMockUser 等注解怎么用?

简单 第 20 / 23 题 更新于 2026/07/28
Spring Security测试WithMockUser安全测试MockMvc

简化版

**测试受 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 = {...})。参数:usernameroles(自动加 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 加载真实用户、用真实角色/权限测)。对比 @WithMockUserMockUser 凭空造(快简单但假)、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」。