Spring Security 集成测试——完整的认证鉴权流程自动化测试方案
Spring Security 集成测试——完整的认证鉴权流程自动化测试方案
适读人群:Java 后端开发者、Spring Security 实践者 | 阅读时长:约 17 分钟 | 核心价值:系统掌握 Spring Security 的集成测试方法,覆盖 JWT 认证、RBAC 权限、接口安全的完整验证
安全代码里的 Bug,是我最怕在生产上发现的一类问题。
有一年,我们上线了一个后台管理系统。上线后两个月,安全团队做渗透测试,发现了一个漏洞:普通员工账号可以访问某个管理员接口,只需要在 URL 里改一下 ID。
查代码,Security 配置里有这样一段:
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/manager/**").hasRole("MANAGER")
// 有一个接口写成了 /api/management/...,不在上面两个 pattern 里,默认放行了/api/management/xxx 不满足任何 pattern,走到了最后的默认规则 anyRequest().authenticated(),只要登录就能访问,没有角色限制。
更糟糕的是,我们原来有安全相关的单元测试,但都是 @WebMvcTest + @WithMockUser,测试的是 Controller 层的逻辑,完全没有测 Security Filter 链的实际行为。Mock 直接绕过了 Filter 链。
那之后,我们建了一套完整的 Spring Security 集成测试体系,覆盖真实的 JWT 验证流程和 RBAC 权限检查。今天这篇,完整写出来。
一、Spring Security 测试的层次
层次一:Security Filter 链测试(最重要)
- 未认证请求是否返回 401
- Token 格式错误/过期是否返回 401
- 权限不足是否返回 403
- 正确权限是否能访问
层次二:业务接口权限测试
- 不同角色对不同接口的访问控制
- URL Pattern 匹配是否完整
层次三:细粒度权限测试
- 用户只能访问自己的数据(行级权限)
- 方法级
@PreAuthorize注解是否生效
二、测试基础设施配置
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public abstract class AbstractSecurityTest {
@Container
@ServiceConnection
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.36");
@LocalServerPort
protected int port;
@Autowired
protected UserRepository userRepository;
@BeforeEach
void setUpBase() {
RestAssured.port = port;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
createTestUsers();
}
@AfterEach
void tearDownBase() {
userRepository.deleteAll();
}
private void createTestUsers() {
// 创建普通用户
userRepository.save(User.builder()
.email("user@test.com")
.password(BCrypt.hashpw("user123", BCrypt.gensalt()))
.roles(Set.of(Role.USER))
.status(UserStatus.ACTIVE)
.build());
// 创建管理员
userRepository.save(User.builder()
.email("admin@test.com")
.password(BCrypt.hashpw("admin123", BCrypt.gensalt()))
.roles(Set.of(Role.USER, Role.ADMIN))
.status(UserStatus.ACTIVE)
.build());
// 创建超管
userRepository.save(User.builder()
.email("superadmin@test.com")
.password(BCrypt.hashpw("super123", BCrypt.gensalt()))
.roles(Set.of(Role.USER, Role.ADMIN, Role.SUPER_ADMIN))
.status(UserStatus.ACTIVE)
.build());
}
// 获取指定用户的 JWT Token
protected String getToken(String email, String password) {
return given()
.contentType(ContentType.JSON)
.body("""
{"email": "%s", "password": "%s"}
""".formatted(email, password))
.when()
.post("/api/auth/login")
.then()
.statusCode(200)
.extract()
.jsonPath()
.getString("accessToken");
}
protected String userToken() { return getToken("user@test.com", "user123"); }
protected String adminToken() { return getToken("admin@test.com", "admin123"); }
protected String superAdminToken() { return getToken("superadmin@test.com", "super123"); }
}三、完整的认证流程测试
class AuthenticationFlowTest extends AbstractSecurityTest {
@Test
void 登录_正确凭证_返回JWT_Token() {
given()
.contentType(ContentType.JSON)
.body("""
{"email": "user@test.com", "password": "user123"}
""")
.when()
.post("/api/auth/login")
.then()
.statusCode(200)
.body("accessToken", notNullValue())
.body("refreshToken", notNullValue())
.body("expiresIn", greaterThan(0))
.body("tokenType", equalTo("Bearer"));
}
@Test
void 登录_错误密码_返回401() {
given()
.contentType(ContentType.JSON)
.body("""
{"email": "user@test.com", "password": "wrongpassword"}
""")
.when()
.post("/api/auth/login")
.then()
.statusCode(401)
.body("error", equalTo("Invalid credentials"));
}
@Test
void 登录_不存在的用户_返回401() {
given()
.contentType(ContentType.JSON)
.body("""
{"email": "notexist@test.com", "password": "password"}
""")
.when()
.post("/api/auth/login")
.then()
.statusCode(401);
}
@Test
void 访问受保护接口_无Token_返回401() {
given()
// 不带 Authorization header
.when()
.get("/api/users/me")
.then()
.statusCode(401)
.body("error", equalTo("Unauthorized"))
.body("message", containsString("No token provided"));
}
@Test
void 访问受保护接口_Token格式错误_返回401() {
given()
.header("Authorization", "Bearer invalid-token-format")
.when()
.get("/api/users/me")
.then()
.statusCode(401)
.body("error", equalTo("Invalid token"));
}
@Test
void 访问受保护接口_Token过期_返回401() {
// 使用提前配置的过期 Token
String expiredToken = generateExpiredToken("user@test.com");
given()
.header("Authorization", "Bearer " + expiredToken)
.when()
.get("/api/users/me")
.then()
.statusCode(401)
.body("error", equalTo("Token expired"));
}
@Test
void RefreshToken_有效_返回新AccessToken() {
// 先登录获取 refreshToken
String refreshToken = given()
.contentType(ContentType.JSON)
.body("""{"email": "user@test.com", "password": "user123"}""")
.when()
.post("/api/auth/login")
.then()
.extract()
.jsonPath()
.getString("refreshToken");
// 用 refreshToken 获取新的 accessToken
given()
.contentType(ContentType.JSON)
.body("""{"refreshToken": "%s"}""".formatted(refreshToken))
.when()
.post("/api/auth/refresh")
.then()
.statusCode(200)
.body("accessToken", notNullValue())
.body("accessToken", not(equalTo(refreshToken))); // 新 token 应该不同
}
}四、RBAC 权限矩阵测试
class RbacPermissionMatrixTest extends AbstractSecurityTest {
private String userToken;
private String adminToken;
private String superAdminToken;
@BeforeEach
void initTokens() {
userToken = userToken();
adminToken = adminToken();
superAdminToken = superAdminToken();
}
// ===== 用户管理接口权限测试 =====
@Test
void 获取用户列表_普通用户_返回403() {
given().header("Authorization", "Bearer " + userToken)
.when().get("/api/admin/users")
.then().statusCode(403);
}
@Test
void 获取用户列表_管理员_返回200() {
given().header("Authorization", "Bearer " + adminToken)
.when().get("/api/admin/users")
.then().statusCode(200);
}
@Test
void 删除用户_管理员_返回403() {
// 管理员不能删除用户,只有超管才能删
given().header("Authorization", "Bearer " + adminToken)
.pathParam("id", 1)
.when().delete("/api/admin/users/{id}")
.then().statusCode(403);
}
@Test
void 删除用户_超管_返回204() {
Long userId = createTempUser("temp@test.com");
given().header("Authorization", "Bearer " + superAdminToken)
.pathParam("id", userId)
.when().delete("/api/admin/users/{id}")
.then().statusCode(204);
}
// ===== 自己的数据,行级权限 =====
@Test
void 获取自己的订单_正常返回() {
given().header("Authorization", "Bearer " + userToken)
.when().get("/api/users/me/orders")
.then().statusCode(200);
}
@Test
void 获取他人的订单_返回403() {
// 普通用户试图访问其他用户的订单
given().header("Authorization", "Bearer " + userToken)
.pathParam("userId", 9999)
.when().get("/api/users/{userId}/orders")
.then().statusCode(403);
}
@Test
void 获取他人的订单_管理员_可以访问() {
// 管理员可以访问任何用户的订单
given().header("Authorization", "Bearer " + adminToken)
.pathParam("userId", 9999)
.when().get("/api/users/{userId}/orders")
.then().statusCode(200);
}
}五、三个踩坑实录
坑 1:@WithMockUser 绕过了 JWT Filter
现象: 用 @WithMockUser 测试通过了,但真实场景下 JWT 验证却有问题。
原因: @WithMockUser 直接向 Spring Security 上下文注入用户信息,完全绕过了 JWT 解析 Filter。Filter 里的 token 提取、签名验证、用户查询逻辑都没有被测到。
解法: 集成测试不要用 @WithMockUser,要像真实用户一样先登录再请求:
// 不要这样
@Test
@WithMockUser(roles = "ADMIN")
void 管理员接口() {...}
// 要这样(真实 JWT 流程)
@Test
void 管理员接口() {
String token = getToken("admin@test.com", "admin123");
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/api/admin/users")
.then()
.statusCode(200);
}坑 2:CSRF Token 导致 POST 请求失败
现象: GET 请求正常,POST/PUT/DELETE 请求都返回 403,日志里没有权限错误,只有 CSRF 错误。
原因: Spring Security 默认开启 CSRF 保护,测试里的 POST 请求没有携带 CSRF Token。
解法:
// 方式一:测试配置里关闭 CSRF(REST API 通常不需要 CSRF)
@TestConfiguration
class TestSecurityConfig {
@Bean
SecurityFilterChain testChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // REST API 不需要 CSRF
// 其他配置...
.build();
}
}
// 方式二:生产配置里就关掉(推荐,REST API 通常用 JWT 而不需要 CSRF)
// application.yml 或 SecurityConfig.java
http.csrf(csrf -> csrf.disable())坑 3:Token 中的用户信息在数据库删除后仍然有效
现象: 测试用的用户账号删除后,用之前获取的 Token 仍然能访问接口。
原因: JWT 是无状态的,服务端不存储 Token。即使用户被删除,Token 在过期之前仍然有效。
解法: 如果需要立即失效,必须在服务端维护 Token 黑名单(或使用短有效期 + 频繁刷新的策略):
// 测试验证 Token 黑名单机制
@Test
void 用户登出后Token失效() {
String token = getToken("user@test.com", "user123");
// 先验证 Token 有效
given().header("Authorization", "Bearer " + token)
.when().get("/api/users/me")
.then().statusCode(200);
// 登出(将 Token 加入黑名单)
given().header("Authorization", "Bearer " + token)
.when().post("/api/auth/logout")
.then().statusCode(200);
// 再次使用同一 Token 访问,应该返回 401
given().header("Authorization", "Bearer " + token)
.when().get("/api/users/me")
.then().statusCode(401)
.body("error", equalTo("Token has been revoked"));
}六、@MethodSecurity 测试
@Test
void 方法级权限_PreAuthorize_生效() {
// 普通用户尝试审批订单(只有管理员才能审批)
Long orderId = createPendingOrder();
given().header("Authorization", "Bearer " + userToken)
.pathParam("id", orderId)
.when()
.post("/api/orders/{id}/approve")
.then()
.statusCode(403)
.body("error", containsString("Access Denied"));
// 管理员可以审批
given().header("Authorization", "Bearer " + adminToken)
.pathParam("id", orderId)
.when()
.post("/api/orders/{id}/approve")
.then()
.statusCode(200);
}Security 配置的集成测试,是防止权限漏洞的最后一道防线。每次修改 Security 配置,必须运行这套测试。
