从零搭建 Spring Security 认证系统,我踩过的那些坑与收获
开篇:为什么选择写这篇关于 Spring Security 的文章?

在我这几年的 Java 全栈开发经历中,Spring Security 是一个绕不开的话题。无论是做内部管理平台、微服务架构下的认证授权中心,还是 ToC 的用户系统,安全机制始终是第一道防线。
最近一次项目中,我们团队接到一个任务:为一个新的 SaaS 平台快速搭建一套可扩展的身份认证体系。时间紧、需求变化快,我们需要在两周内完成原型并上线演示版本。
这个时候,我第一个想到的是——用 Spring Security + OAuth2 来搞定核心认证流程。虽然之前也踩过不少坑,但这次经验尤其值得总结。于是就有了这篇文章。
希望这篇文章能给刚接触 Spring Security 或者想快速上手的同学们带来一些实用的经验和思路。
问题描述:项目背景与挑战

我们的项目是一个面向中小企业的 SaaS 平台,目标是为企业提供统一的身份登录入口,并集成后续的数据分析、CRM、客户管理等多个子系统模块。
项目初期有三个核心诉求:
- 实现基本的用户名+密码登录;
- 后续需要对接第三方平台(如钉钉、企业微信)的认证;
- 需要区分不同角色权限(普通员工、管理员、超级管理员);
听起来不复杂?其实不然。当时我们遇到几个关键问题:
- 如何设计数据库结构来支持灵活的角色权限?
- Spring Security 配置太复杂了,各种 Filter、Provider 怎么理解?
- 登录接口老是返回 403 或 401,找不到具体原因;
- 想把 JWT 和 Session 结合起来用,但网上资料五花八门,选型困难;
- 单元测试怎么覆盖这些认证逻辑?
这些问题看似常见,但在实际操作中很容易陷入细节的泥潭。
解决方案:基于 Spring Security 的分层认证设计

我们最终选择了这样的技术栈:
- Spring Boot 2.7.x
- Spring Security 5.x
- Spring Data JPA(MySQL)
- OAuth2(用于后期扩展)
- JWT + Redis(短期 Token 管理)
整个系统的认证流程大致如下:
用户 -> 登录接口 -> 身份验证成功 -> 生成 JWT + 存入 Redis -> 带着 Token 请求其他接口
同时,我们设计了一个灵活的角色模型,支持 RBAC(基于角色的访问控制)模式。
接下来我会结合代码讲解实现细节。
代码实践:一步步搭建基础认证体系
第一步:引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
第二步:设计用户和角色实体类
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}
@Entity
public class Role {
@Id
private String name; // 如 ROLE_ADMIN, ROLE_USER
}
第三步:自定义登录接口和 JWT 工具类
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
String token = jwtUtil.generateToken(authentication);
return ResponseEntity.ok().header("Authorization", "Bearer " + token).build();
}
}
JWT 工具类这里就不贴完整实现了,主要功能包括:
- 生成 Token(带用户名、角色信息等 claim)
- 解析 Token 并提取认证信息
- 校验签名是否有效
第四步:配置 Spring Security
这是最核心的部分,也是最容易出错的地方。
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/auth/login").permitAll()
.anyRequest().authenticated();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
这个配置文件干了几件事:
- 禁用了 CSRF(适用于前后端分离项目)
- 使用无状态会话(配合 JWT)
- 添加了 JWT 拦截器到过滤链中
- 放开
/login接口访问限制 - 设置密码加密方式(BCrypt)
第五步:实现 JWT 拦截器
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return;
}
}
filterChain.doFilter(request, response);
}
}
踩坑经验:那些调试时崩溃的夜晚
1. 登录总是失败:AuthenticationManager 不工作?
这个问题困扰了我大半天。后来发现是因为我没有注册 AuthenticationManager Bean,或者 UserDetailsService 没有被正确注入。
解决方案:
确保在配置中正确注册 AuthenticationManager bean,并且你的 UserDetailsService 实现类必须标注为 @Service,并在加载时注册进 Spring 容器。
2. 权限不起作用,明明配置了 hasRole('ADMIN') 却可以访问?
注意 Spring Security 中对 role 的前缀处理,默认会自动加上 "ROLE_"。例如:
.antMatchers("/admin/**").hasRole("ADMIN")
// 实际等价于 ROLE_ADMIN
所以你存储角色的时候不要加 ROLE_ 前缀,Spring 会帮你自动拼接。
如果你使用 hasAuthority() 则不会自动加前缀,你需要明确写出完整的 authority 名称。
3. 登录成功后跳转失败或直接 403?
这一般是由于没有正确设置登录成功的回调 handler。可以重写 successHandler 或者直接返回 JSON 数据。
我们在实际开发中选择了直接返回 JSON 数据,省去页面跳转相关的配置。
4. 单元测试怎么写?
Spring 提供了 MockMvc,你可以模拟认证请求,比如:
mockMvc.perform(post("/api/auth/login")
.content("{ \"username\":\"test\", \"password\":\"test\" }")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
但要注意:每次测试完都要清空 SecurityContext,防止影响下次测试。
效果总结:落地后的收益和提升
项目上线之后,我们获得了以下几个显著的好处:
- 快速构建了一套可扩展的认证系统,后续接入 OAuth2 只需增加几个 Filter 即可;
- 用户角色清晰,权限控制粒度细化到接口级别;
- 系统安全性大大增强,未出现一次账号越权访问问题;
- 登录性能稳定,单节点 QPS 约 3k 左右;
- 日志齐全,便于排查异常行为。
此外,在运维方面我们也积累了一些经验:
- 频繁登录失败 IP 会被触发频率限制(Redis+IP计数器);
- 登录成功后会记录审计日志,用于后期追溯;
- 使用 APM(如 SkyWalking)监控认证链路性能。
经验分享:写给正在路上的你
✅ 架构建议:
- 把认证中心抽出来作为独立服务,有利于微服务治理;
- 使用 Redis 缓存 Token,支持吊销/刷新;
- 多租户系统考虑使用多级缓存(本地 Caffeine + Redis)提升性能;
- 对敏感操作加入双因素认证(2FA)或短信验证码。
💡 小贴士:
- 不要重复造轮子,Spring Security 社区文档是最权威的参考资料。
- 多打印日志!尤其是 Filter 链执行顺序和 SecurityContext 的变化。
- 遇到问题先看 Spring Security 自动配置的内容,再决定是否手动修改。
- 如果你是新手,建议从最简单的表单登录开始练手,逐步深入。
🧭 展望未来:
随着云原生和微服务的发展,身份认证的边界也在不断演化。像 OAuth2 + OpenID Connect、Keycloak、Auth0、Casdoor 这样的产品或协议,已经成为行业主流。我们在本次项目中打好了基础,也为下一步接入外部认证做好了准备。
结语

写这篇文章的过程中,我仿佛又回到了那个调试深夜、咖啡喝到第三杯、终于看到 “Login successful” 的瞬间。
Spring Security 是一个强大的工具,但也因为其灵活性而显得“难啃”。不过只要我们从实际业务出发,一步步搭建,就一定能够掌握它。
如果你也在学习 Spring Security,不妨动手试试上面的例子。有问题欢迎留言讨论!
希望我的实战经验对你有所帮助。下期见 👋
📌 附:GitHub 示例仓库地址(假设存在)
你可以在这个 Repo 中找到完整的示例工程: 👉 https://github.com/zhangsan/spring-security-demo
(注:以上内容基于真实项目重构简化而来,部分敏感信息已脱敏处理。)

评论 0