从零搭建安全认证系统:Spring Security 实战经验分享
作为一名拥有五年工作经验的后端工程师,我参与过多个企业级系统的开发与维护。其中,构建用户权限体系、实现登录认证和接口访问控制一直是我工作中最核心、也是最容易出问题的部分之一。
今天我想聊的是——如何用 Spring Security 快速搭起一套安全、灵活、可扩展的安全认证系统。这不是什么“Hello World”级别的教程,而是基于我去年在某金融 SaaS 平台重构权限模块的真实经历来写的。
背景介绍:为什么我们需要重新做认证系统?

我们原来的认证逻辑比较简单粗暴:用户登录后返回一个 token,后面所有接口都靠这个 token 去鉴权。但随着业务发展,我们遇到了几个明显的问题:
- token 过期管理混乱:前端缓存 token 导致频繁出现 401(未授权),用户体验差;
- 缺乏权限分层机制:不同角色(比如客服、管理员、运营)之间没有清晰的访问边界;
- 日志追踪困难:谁干了什么事,无从追溯;
- 代码结构复杂:各种 if-else 判断权限、手写 JWT 解析逻辑,一改就出 bug。
这些问题逼着我们必须重新梳理整套安全架构,而 Spring Security 就是我们最终选择的核心工具。
选型对比:为什么是 Spring Security,而不是 Shiro 或 Sa-Token?

在我过去项目中,我用过 Apache Shiro、也试过国产框架 Sa-Token,但这次我毫不犹豫地选择了 Spring Security,原因如下:
| 框架 | 易用性 | 可扩展性 | 社区活跃度 | 集成生态支持 |
|---|---|---|---|---|
| Spring Security | 中 | 极强 | 非常高 | Spring Boot 天生一对 |
| Apache Shiro | 高 | 一般 | 低 | 不适合大型系统 |
| Sa-Token | 高 | 一般 | 中 | 国产轻量、文档友好 |
虽然 Spring Security 学习曲线陡峭一些,但它提供的细粒度控制能力太吸引人了。尤其是在处理 OAuth2、JWT、前后分离登录流程、多角色授权方面,它提供了非常成熟的解决方案,几乎涵盖了我们所有的使用场景。
实施过程:一步步搭建安全认证体系

Step 1:需求梳理与接口设计
我们在重构之前,先开了一场头脑风暴会,明确以下几点:
- 用户类型有三种:平台管理员、客户管理员、普通员工
- 系统资源需区分读写权限(查看报表 vs 编辑合同)
- 支持单点登出和 Token 续签
- 接口要区分公开访问(如注册页)、需登录访问、需特定权限访问
基于这些需求,我们将整个安全体系分为三个层次:
- 身份认证层(Authentication):验证用户是否合法;
- 授权层(Authorization):确定用户能干什么;
- 访问控制层(Access Control):根据 URL/方法进行拦截或放行。
Step 2:引入 Spring Security Starter
我们的项目是 Spring Boot 2.7.x 的版本,直接引入 starter 即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
默认情况下,Spring Boot 会自动注入一个 Basic Auth 的安全机制,所有接口都会被保护。这对我们来说当然是不够的,所以需要自定义配置。
Step 3:自定义 WebSecurityConfigurerAdapter(注意:已废弃)
虽然现在官方推荐使用 SecurityFilterChain API(Spring Boot 2.7+ 开始逐渐迁移),但我们老项目还是用了经典的 WebSecurityConfigurerAdapter,如果你也在迁移到新版本,可以尝试下面这种写法:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();
return http.build();
}
}
这种方式相比以前的 adapter 类更简洁,也更适合函数式编程风格。大家可以根据项目实际情况选用。
Step 4:JWT + Redis 实现 Token 机制
我们采用了 JWT + Redis 的组合方案:
- 登录成功后生成 JWT Token,并将该 Token 存入 Redis 设置 TTL;
- 后续每次请求携带 Token,解析用户信息;
- 如果 Token 过期或被注销,则删除 Redis 中记录;
- 每次访问前通过拦截器校验 Token 是否有效。
JWT 工具类大致如下(只展示关键部分):
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
Redis 的作用主要是为了实现黑名单机制和 Token 注销功能。如果只用 JWT 是无法做到真正意义上的“注销”的。
Step 5:权限管理细化到角色+接口级别
我们参考了 RBAC 模型(基于角色的访问控制),在数据库中设计了以下几个表:
-- 角色
roles (id, name)
-- 权限
permissions (id, name, code)
-- 角色和权限关联
role_permissions (role_id, permission_id)
-- 用户和角色关联
user_roles (user_id, role_id)
然后在每个 Controller 上加上注解:
@PreAuthorize("hasPermission('contract:write')")
@PostMapping("/contract")
public ResponseEntity<?> createContract(@RequestBody ContractDTO dto) {
// ...
}
这里我们用到了 SpEL 表达式配合自定义权限判断器来控制访问,具体实现可以继承 PermissionEvaluator 接口。
Step 6:自定义异常处理 + 日志埋点
为了让安全拦截更“友好”,我们增加了统一的异常处理:
@RestControllerAdvice
public class SecurityExceptionAdvice {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<?> handleAccessDenied() {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("无权操作");
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<?> handleAuthError() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("身份验证失败");
}
}
同时,在 Filter 中埋点记录用户行为:
String username = getUsernameFromToken(request);
log.info("User [{}] accessed URI: {}", username, request.getRequestURI());
这些日志帮助我们快速定位权限问题,对审计也非常有用。
Step 7:生产环境上线后的调优建议
我们部署后遇到不少坑,总结一下实战经验:
1. 数据库连接池设置合理
因为权限校验涉及数据库查询,我们一开始没做好连接池配置,导致并发时大量请求阻塞。后来改成了 HikariCP,并做了缓存优化:
- 使用本地缓存缓存角色权限映射;
- 减少每次请求都要查 DB 的频率;
- 加上 Redis 缓存失效策略。
2. Token 有效期不宜过长
早期我们把 token 设为 7 天过期,结果出现了 token 泄露风险高、强制下线不好实现等问题。最终我们调整为 2 小时 + refresh token,refresh token 在 Redis 控制生命周期。
3. 日志不能忽略
安全相关的所有动作必须记录日志,包括登录失败、访问拒绝、token 过期等。日志字段建议包含:
- 用户名
- 请求路径
- 时间戳
- IP 地址
- traceId
这样在排查问题时才能迅速定位。
成果与收益

经过一个月的开发与测试,我们完成了完整的安全认证系统重构:
- 所有接口都有清晰的权限管控;
- 登录流程稳定,支持 JWT + Refresh Token;
- 异常处理统一规范,提升接口友好性;
- 安全漏洞减少,审计日志完整;
- 新增角色只需后台配置,不需要改代码。
后续我们也在此基础上接入了 OAuth2 第三方登录,甚至开始探索基于 Open Policy Agent(OPA)实现更动态的授权策略。
我的经验和小建议
不要怕踩坑,但要避免重复踩坑
Spring Security 确实难,但它是“一旦掌握,终身受用”。别急着抄 GitHub 上的 demo,一定要理解它的设计思想(Filter Chain、SecurityContext、Principal 等)。不要一开始就追求完美
安全系统是一个长期演进的过程。先完成基本认证、登录、权限划分,再去考虑复杂的扩展点(如多租户、外部认证等)。权限模型尽量抽象清晰
权限码的设计最好统一格式,例如:{模块}:{操作},如user:read,order:delete,方便后期对接第三方权限系统。性能问题早暴露早解决
认证、鉴权过程涉及多次数据库查询,务必做好监控,特别是线上高峰时段的表现。技术是手段,不是目的
有时候我们会沉迷于技术细节,而忘了用户的实际需求。比如是否真的需要那么复杂的权限控制?或者是否可以通过业务流程限制代替技术权限?
写在最后
写这篇文章除了想分享我的实战经验,其实还想鼓励一下刚入行的同学:Spring Security 虽然难,但它是值得你花时间去攻克的一座“高山”。
我也曾经面对满屏的 filterOrder() 和一堆 .and() 的配置感到无从下手,但现在回头看,这一切都是值得的。
在这个越来越重视数据安全和隐私合规的时代,一个强大而可控的安全认证系统,不仅是项目的护城河,更是你个人技术成长路上的重要里程碑。
希望这篇文章对你有所帮助。如果你有任何想法或疑问,欢迎留言交流!

评论 0