用 Spring Security 快速搭建安全认证系统,我在实际项目中踩过的坑与成长
开篇:为什么我要写这篇关于 Spring Security 的文章?

去年年底,我参与了一个企业级 SaaS 平台的后端重构项目。这个平台已经有几年历史了,原来的权限模型非常薄弱,用户管理、角色分配都分散在各个业务模块中,导致权限控制混乱、接口安全性低,甚至出现了几个生产级别的越权访问漏洞。
我们决定从架构层面彻底重构认证授权体系。在调研了几种方案之后,我们最终选择了 Spring Security + OAuth2 + JWT 这一组合来构建新的安全中心。这期间我遇到了不少问题,也学到了很多经验。
今天,我想以自己的实战经历为基础,和大家一起探讨如何用 Spring Security 搭建一个灵活、可扩展、性能良好的安全认证系统。内容不仅包括基础搭建流程,还包括我在真实开发过程中的思考、踩过的坑和解决思路。
项目背景:SaaS 平台的安全性挑战

系统概述
这是一个面向中小企业的财税管理系统,包含财务核算、发票管理、人事工资等多个模块。平台采用前后端分离结构(前端为 Vue.js,后端为 Spring Boot)。
原有痛点
- 用户登录逻辑分散在多个微服务中,存在重复代码
- RBAC 权限模型未统一,权限校验逻辑耦合严重
- 接口缺乏统一的权限控制机制
- 没有统一的 Token 管理方式,Token 注销和过期难以处理
面对这些问题,我们需要一套能够支撑多租户、支持第三方接入、具备良好扩展性的统一安全体系。
面临的挑战
在初期尝试使用 Spring Security 构建认证中心时,我们遇到了以下几个关键问题:
- 权限粒度过粗:传统的
hasRole或hasAuthority已无法满足复杂的业务需求。 - Token 维护困难:JWT 是无状态的,一旦签发就很难注销或更新,存在安全隐患。
- 跨服务认证问题:多个微服务如何共享同一个认证结果?
- 性能瓶颈预警:权限查询频繁访问数据库,影响响应速度。
- RBAC 模型设计不合理:原有的角色权限绑定过于僵化,无法应对多租户场景下的动态配置。
这些问题让我意识到:单纯的“照搬教程”是不够的,必须结合实际架构进行优化。
解决方案:Spring Security 构建统一认证中心
我们采用了以下技术栈和架构设计:
- 核心框架:Spring Boot 2.7 + Spring Security 5.6
- 认证方式:基于 OAuth2 的资源服务器 + 客户端凭证模式
- Token 管理:JWT + Redis 缓存黑名单(用于注销)
- 数据库:MySQL 存储用户信息、角色、权限关系
- 缓存:Redis 用于存储活跃 Token 和权限缓存
- 微服务通信:OpenFeign + 自定义拦截器完成上下文传递
整体架构如下:
+------------------+ +------------------+
| Gateway | | Auth Server |
| --|--------| |
+------------------+ +--------+---------+
|
+------------------------------v----------------------------+
| Microservices (Resource Servers) |
| User APIs / Invoice APIs / HR APIs ... |
| Each service validates token via JWT |
+----------------------------------------------------------+
所有服务通过网关对外暴露 API,网关将请求转发前验证 Token,并携带用户上下文信息到具体微服务中。真正的权限校验由各微服务自身完成。
关键实现:一步步搭建 Spring Security 安全认证系统

1. 初始化项目结构
我选择的是 Spring Initializr 创建标准的 Spring Boot 项目,主要依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 实现核心过滤器链
Spring Security 的本质是一个过滤器链条,我们可以自定义其中的关键步骤。
自定义 JWT 登录过滤器
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
public JwtLoginFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/api/auth/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
// 从请求体读取用户名密码
try {
LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) {
String token = jwtUtil.generateToken(authResult.getName());
response.addHeader("Authorization", "Bearer " + token);
}
}
自定义 JWT 请求拦截器
public class JwtRequestFilter extends OncePerRequestFilter {
private final UserService userService;
private final JwtUtil jwtUtil;
public JwtRequestFilter(UserService userService, JwtUtil jwtUtil) {
this.userService = userService;
this.jwtUtil = jwtUtil;
}
@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);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
3. 配置 Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserService userService;
private final JwtUtil jwtUtil;
public SecurityConfig(UserService userService, JwtUtil jwtUtil) {
this.userService = userService;
this.jwtUtil = jwtUtil;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(new JwtLoginFilter(http.getSharedObject(AuthenticationManager.class), jwtUtil))
.addFilterBefore(new JwtRequestFilter(userService, jwtUtil), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userService)
.build();
}
}
踩坑经验分享
1. Token 注销难?—— 加入 Redis 黑名单机制
一开始我们直接使用 JWT,发现一旦签发后几乎无法撤销权限,尤其是当用户退出登录或账户被禁用时,旧 Token 仍可能继续有效一段时间。
解决方案:引入 Redis 黑名单机制。
每次签发新 Token 时,记录其 jti(唯一标识符),设置 TTL 为剩余有效期。在每次请求拦截时先检查该 Token 是否在黑名单中。
if (isTokenBlacklisted(token)) {
throw new InvalidJwtException(JwtException.NO_TOKEN, "Token 已被注销");
}
2. 权限判断太慢?—— 引入缓存策略
由于每次接口调用都需要校验权限,频繁地访问数据库会造成延迟。我们对常用权限信息做了两级缓存(Spring Cache + Redis)。
@Cacheable(value = "permissions", key = "#userId")
public List<String> getUserPermissions(Long userId) {
// 查询数据库获取权限列表
}
同时结合 AOP,在需要校验的接口上加上注解:
@HasPermission("can_edit_invoice")
@GetMapping("/invoices/{id}/edit")
public ResponseEntity<?> editInvoice(@PathVariable Long id) {
...
}
3. 多租户场景下角色冲突?—— 动态权限模型设计
我们的系统支持不同租户的管理员独立配置角色权限。这意味着同一名为“admin”的角色在不同租户下可能有不同的权限。
为此,我们在 JWT 中加入了 tenant_id 字段,并在权限加载时根据当前租户来过滤权限数据:
{
"sub": "admin@example.com",
"tenant_id": "12345",
"roles": ["TENANT_ADMIN"],
...
}
4. 日志追踪丢失用户上下文?—— 结合 MDC 实现用户追踪
为了更好地排查问题,我们在每个请求拦截阶段都把用户信息放入 MDC(Mapped Diagnostic Context),方便日志追踪:
MDC.put("user", auth.getName());
这样,日志文件就能清晰地标明操作者是谁,极大提升了问题定位效率。
效果与收益总结
经过两个月的迭代,我们成功上线了全新的认证授权系统,带来显著提升:
- 安全性增强:杜绝了越权访问,实现了统一权限控制
- 开发效率提升:权限代码复用率提升 80% 以上,减少了重复劳动
- 性能优化:平均请求耗时减少约 30%,缓存命中率达 95%
- 可扩展性强:新增租户、角色、权限无需修改底层逻辑,只需修改配置即可生效
- 运维成本下降:权限管理可视化程度提高,日志追踪更清晰
最重要的是,整个团队对 Spring Security 的掌控能力大幅提升,不再局限于“会用”,而是能深入理解其原理并做出合理调整。
我的几点建议
如果你也正准备使用 Spring Security 构建安全认证系统,这里是我走过弯路后的一些忠告:
1. 不要盲目复制模板代码
很多网上的示例都是“开箱即用”风格,但不一定适合你的业务场景。务必理解每一步的目的和作用,否则遇到问题时很容易懵圈。
2. 提前规划权限模型
权限模型的设计往往决定了整个系统的可维护性。推荐参考经典的 RBAC 模型,但也要根据业务特点适当扩展,比如加入多租户支持、细粒度权限等。
3. 把握好“集中 vs 分散”
虽然我们要追求统一,但在微服务架构下,权限判定其实最好下沉到业务层来做。认证可以统一,但具体权限应由各服务自行负责,避免耦合过度。
4. 性能考虑要前置
权限校验涉及数据库查询和 Token 验证,尤其在高并发下极易成为瓶颈。提前规划缓存、连接池、异步刷新策略至关重要。
5. 做好异常处理和日志审计
Spring Security 默认的异常返回格式很原始,建议自定义异常处理器,让错误信息更友好。此外,一定要记录关键事件,如登录失败、Token 注销、越权尝试等。
写在最后:技术人的安全感来自不断实践
说实话,在刚接手这个任务的时候,我心里也没底。毕竟 Spring Security 虽然名气大,但真的要用起来却并不轻松。它的灵活性和复杂性是一体两面:你既可以很快上手,也可以深陷其中。
但正是通过这次实战,我对权限体系的理解更加透彻,也开始学会从架构视角去权衡各种设计方案。这种成长比单纯的“学会某个工具”更有价值。
希望这篇文章能帮助你在搭建 Spring Security 安全系统的过程中少走些弯路。如果你也在做类似的事情,欢迎留言交流,一起进步。
毕竟,我们都在路上。

评论 0