Spring Security实战:一个后端工程师的认证系统搭建手记
开篇:为什么我要写这篇文章?

作为一名有五年工作经验的后端工程师,我参与过多个中小型项目的开发与维护工作。其中一个让我印象最深的项目,是去年为一家金融公司重构其后台系统的用户权限模块。当时我们决定使用Spring Security来实现安全认证机制。
说实话,那时候我对Spring Security的理解还停留在“听说过”的阶段。在真正上手之后才发现,这个框架虽然功能强大、扩展性强,但它的学习曲线也并不平缓。尤其在面对实际业务场景时,很多细节处理不当就可能带来严重的安全隐患或性能问题。
所以我希望借这篇文章,把我这段时间的踩坑经历、解决方案和心得总结分享出来,帮助像我当初一样刚入门的同学少走一些弯路。
问题描述:认证系统搭建前的混乱局面

我们的项目最初是一个遗留系统,原有的用户认证部分完全是手动拼接出来的逻辑:
- 登录接口直接用明文校验用户名密码
- 权限控制基本靠前端判断(你没看错)
- 用户角色信息存在session里,没有做持久化存储
- 没有任何日志记录和失败尝试次数限制
- 前后端混杂,接口权限难以统一管理
这种做法在系统用户量还小的时候勉强能应付,但随着产品逐渐上线推广,暴露的问题越来越多。尤其是在一次安全扫描中,我们被指出了几个高危漏洞,包括:
- 密码未加密存储
- 存在暴力破解风险
- 缺乏访问审计能力
- 接口没有严格的权限隔离
这让我们不得不正视系统的安全性问题,于是启动了用Spring Security重构认证授权体系的计划。
解决方案:引入Spring Security进行整体改造

我们的目标很明确:在不影响现有系统稳定性的前提下,逐步引入Spring Security,提升系统安全性。以下是我们在改造过程中采用的一些关键策略:
✅ 技术选型与架构设计
- 使用 JWT 替代 Session 实现无状态认证
- 结合 Redis 管理令牌的吊销和有效期
- 数据库层面增加了 用户角色权限表结构
- 使用 Spring Data JPA 做数据持久层
- 接口鉴权统一通过 Spring Security Filter Chain 控制
🧩 核心流程
登录 → 生成 JWT → Redis 缓存 → 后续请求带 Token → Filter 解析验证 → 授权访问对应资源
代码实践:如何快速构建认证系统

下面我会从实际工程角度出发,列出几个核心配置类和关键代码片段,并附上一些必要的说明。
🧱 表结构设计
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
create_time DATETIME
);
CREATE TABLE sys_role (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL
);
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id INT NOT NULL,
FOREIGN KEY (user_id) REFERENCES sys_user(id),
FOREIGN KEY (role_id) REFERENCES sys_role(id),
PRIMARY KEY (user_id, role_id)
);
提示:建议将用户敏感字段(如密码)单独存储在一个独立数据库中,以提高数据安全性。
🔐 用户实体与权限集成
@Entity
@Table(name = "sys_user")
public class User implements UserDetails {
@Id
private Long id;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "sys_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> authorities;
// 忽略其他getter/setter
}
这里的关键点是 UserDetails 接口的实现,以及多对多关系映射到Spring Security所需的 GrantedAuthority 集合。
⚙️ 配置类核心设置
@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();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这段代码定义了整个安全过滤链的基础结构,包括:
- 禁用 CSRF
- 设置为无状态模式
- 加入 JWT 自定义过滤器
- 配置 URL 访问权限规则
📡 JWT 工具类实现(简化版)
@Component
public class JwtTokenProvider {
private final String JWT_SECRET = "your-secret-key";
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 86400000))
.signWith(SignatureAlgorithm.HS512, JWT_SECRET)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token);
return true;
} catch (JwtException ex) {
// log error
}
return false;
}
}

👤 认证处理器
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

踩坑经验:那些年我和Spring Security一起犯过的错
❌ 初期忽略的角色加载问题
刚开始我们以为只要返回了 UserDetails 就可以自动拿到对应的权限列表,结果发现接口一直报 403 Forbidden,后来排查才意识到是因为:
如果你的角色信息不是直接通过内存配置的,而是从数据库查询来的,那么一定要确保
UserDetails的getAuthorities()返回值正确!
这个问题花了一个上午定位,教训是:不要忽视Spring Security内部的权限传递流程。
❌ 忘记关闭 Session 并导致 JWT 无效
我们在使用 JWT 之前忘记禁用 Session,结果有些接口仍然会在响应头中出现 Set-Cookie,导致客户端误以为是有状态服务。
解决方式是加上如下配置:
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
❌ 多 Filter 配置顺序错误
Spring Security 的过滤器链顺序非常关键。比如我们在添加 JWT 过滤器时放错了位置,导致某些请求根本没有经过 JWT 的解析,从而出现匿名访问问题。
正确的做法是:
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
这样能保证在执行默认的用户名密码认证之前,先完成自定义的身份识别。
❌ 忽视注销机制导致 Token 可能长期有效
一开始我们只做了 Token 的颁发,却没有处理注销机制。后果就是一旦 Token 泄露,它在整个生命周期内都可正常使用。
后来通过 Redis 缓存 Token 的黑名单(吊销列表),配合拦截器提前检测 Token 是否合法,实现了更完善的注销逻辑。
效果总结:改造后的收益
经过几周的重构后,我们的认证系统发生了明显变化:
| 维度 | 改造前 | 改造后 |
|---|---|---|
| 安全性 | 极低,存在高危漏洞 | 满足 OWASP 基本标准 |
| 性能 | 登录过程慢,依赖 session 锁 | 全部无状态,吞吐量上升约 30% |
| 扩展性 | 新增权限困难 | 角色权限统一管理,新增角色只需 DB 操作 |
| 日志追踪 | 几乎无审计日志 | 登录登出/失败尝试均有完整记录 |
不仅如此,我们还借此机会完成了以下几个改进:
- 引入登录失败锁定机制(支持动态阈值)
- 增加密码复杂度限制
- 为不同角色分配不同的 API 接口权限
- 实现基于 IP 的白名单访问控制(用于管理员操作)
这些优化让整个系统的安全性有了质的飞跃。
经验分享:给正在路上的你的一些建议
作为一个经历过这一套流程的老兵,我想给刚接触Spring Security或者打算重构认证系统的同学几点建议:
🔑 1. 安全意识比框架更重要
掌握Spring Security不是最终目的,重要的是理解安全背后的基本原则:
- 最小权限原则
- 输入验证原则
- 明确身份与权限分离
- 日志不可缺位
- 注销机制必须完善
框架只是工具,安全才是核心。
💡 2. 不要急着追求完美架构
特别是对于已经有大量业务逻辑的项目来说,重构认证模块应该循序渐进:
- 先从登录/登出开始接入 Spring Security
- 再逐步替换原有硬编码的权限逻辑
- 最后统一接口的权限注解管理
每一步都要经过充分测试和灰度发布。
🛠️ 3. 善于利用调试工具
- 使用 Postman 测试接口的权限行为
- 查看 Security 的 Debug 输出,确认过滤链是否触发正确
- 利用 Spring Boot Actuator 查看实时的权限状态
记住:Spring Security 默认是“拒绝一切”,所以一开始可能会觉得处处受限,但这是好事。
🧪 4. 编写单元测试验证权限逻辑
尤其是涉及 RBAC(基于角色的访问控制)的部分,可以通过 MockMvc 编写模拟请求测试不同角色的访问结果:
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void testAdminAccessOnlyApi() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk());
}
这种方式可以有效防止因权限修改带来的回归风险。
🔄 5. 和前端协作好 Token 的管理机制
前后端在处理 Token 生命周期时需要高度同步,比如:
- Token 有效期与刷新时机
- 登录超时跳转逻辑
- Token 自动刷新机制
- 多 tab 页面间共享 Token 的处理
否则容易出现权限混乱的情况。
结语:安全永无止境,进步不止于此
回头来看,那次Spring Security的重构不仅仅是一个技术升级,更像是我们团队安全意识集体觉醒的过程。通过这次改造,我们不仅解决了眼前的隐患,也建立了一套可持续发展的权限治理体系。
现在再回过头来看,如果你问我 Spring Security 到底难不难?我觉得答案取决于你是想用它跑通个 Demo,还是真正让它成为保障系统安全的防线。
框架易学,安全不易。
希望你在学习 Spring Security 的过程中,也能建立起对系统安全更深的理解。别怕踩坑,每一次折腾其实都是成长的机会。
最后,送大家一句话:
“一个好的程序员不仅要写出别人看不懂的代码,更要写出别人不敢轻易攻击的系统。”
共勉 😊
如果你也在实践中遇到了类似问题,欢迎留言讨论。

评论 0