从零开始搭建 Spring Security 认证系统:我的实战经验和思考
开篇:为什么是 Spring Security?
作为后端开发工程师,我经常遇到这样一个问题:在项目初期快速实现一个用户登录认证功能。虽然看起来是个很常规的需求,但在实际开发中却常常因为各种小细节而踩坑。比如权限管理、跨域支持、令牌刷新机制等问题。
在我们团队接手的一个内部管理系统重构项目中,我负责的是安全模块的搭建。这个项目需要为公司多个业务线提供统一的认证服务和权限控制,要求高可用、高扩展性,同时还得兼顾性能。这个时候,Spring Security 成为了首选框架。
本文将结合我在这次项目中的实践经验,手把手带你了解如何快速搭建一个安全、可控、灵活的认证系统,并分享我在过程中踩过的坑和学到的经验。
项目背景与需求分析

我们公司的系统原本使用的是自研的权限模型,结构松散,缺乏统一的认证机制。随着业务扩展,接入的系统越来越多,老的认证方式已经无法支撑现有的增长速度和安全要求。于是,我们决定在新版本中引入一套完整的认证授权体系。
我们的目标是:
- 支持多种认证方式(用户名密码、手机号验证码、第三方OAuth)
- 灵活的权限控制(基于角色/方法级的细粒度控制)
- 高性能和高并发支持
- 支持 JWT 无状态认证
- 可扩展性强,方便对接其他微服务或 SSO 系统
遇到的挑战

刚接触 Spring Security 的时候,我觉得它就是一个用来做登录拦截的“安全插件”,结果上手之后才发现远远不止这些。Spring Security 框架非常庞大且灵活,但正因为灵活性太高,导致文档复杂,学习曲线陡峭。
我们团队在搭建初期遇到了几个关键问题:
1. 过滤器链配置混乱,请求被拦截或放行不当
我们在集成 JWT 和传统表单登录时,发现请求总是被拦截或者跳过验证,调试了好久才弄明白各个过滤器之间的执行顺序和逻辑关系。
2. 授权粒度过粗,不能满足业务需求
最初用简单的 hasRole 方法控制接口访问权限,结果发现很多场景下需要更细粒度的控制,比如某个角色下的某类操作是否允许。
3. 权限信息存储设计不合理
一开始想图省事直接用数据库字段保存权限列表,后来发现维护成本高、扩展性差,必须重新设计。
技术方案与实现思路

针对这些问题,我们逐步构建了一套稳定、可维护的安全认证体系。下面我会详细拆解每个核心部分的实现。
1. 架构概览
整个安全模块分为以下几层:
前端 <-> [Gateway] <-> [Auth Service] <-> [User Center]
↘
[Resource Server]
在这个架构中:
- Gateway 负责全局鉴权校验(如 Token 合法性),防止无效请求打到后端。
- Auth Service 是核心认证中心,处理登录、登出、Token生成等。
- User Center 提供用户信息和权限数据查询接口。
- Resource Server 是各业务系统的后端服务,依赖 Gateway 做前置鉴权,自己也可做更细粒度的判断。
这种设计可以有效解耦业务系统和权限系统,便于后续扩展。
2. 登录流程设计(Username + Password)
以最基础的用户名密码为例,我们搭建的流程如下:
- 用户发送
/login请求携带用户名和密码; - Auth Service 接收请求后,调用 User Detail Service 查询用户信息;
- 使用 BCryptPasswordEncoder 对比密码;
- 密码正确则生成 JWT 并返回;
- 客户端后续请求携带 JWT 在 Header 中;
- Gateway 或 Resource Server 校验 Token 并解析出用户信息;
- 结合权限信息进行最终访问控制。
关键代码:JWT 工具类
public class JwtUtils {
private static final String SECRET = "your-secret-key";
private static final long EXPIRATION = 86400000; // 1 day
public static String generateToken(String username, List<String> roles) {
return Jwts.builder()
.setSubject(username)
.claim("roles", roles)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public static String parseUsername(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody().getSubject();
}
public static List<String> parseRoles(String token) {
return (List<String>) Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token)
.getBody().get("roles");
}
}
3. 整合 Spring Security 实现 Token 认证流程
为了让 Spring Security 支持 JWT,我们需要自定义 OncePerRequestFilter 去解析 Token 并设置当前用户上下文。
自定义 TokenFilter
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = extractToken(request);
if (token != null && validateToken(token)) {
String username = JwtUtils.parseUsername(token);
List<String> roles = JwtUtils.parseRoles(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, null, AuthorityUtils.createAuthorityList(roles.toArray(new String[0]))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(JwtUtils.SECRET).parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
然后在配置中加入这个过滤器:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这样我们就实现了完整的 Token 认证流程。
4. 权限控制设计与优化
前面提到,默认的 hasRole() 控制太粗,我们需要更细粒度的控制方式。我们采用了两种策略:
4.1 方法级别的权限注解
使用 @PreAuthorize 注解来控制接口访问权限:
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('read_user') or hasRole('ADMIN')")
public User getUser(@PathVariable Long id) {
// ...
}
}
这种方式非常清晰,也方便后期管理和审计权限。
4.2 动态权限加载
为了避免每次修改权限都要重启服务,我们设计了一个动态权限更新机制:
- 将权限信息缓存到 Redis 中
- 每次请求通过 AOP 获取接口上的权限注解并检查
- 如果 Redis 缓存失效,异步加载最新权限信息
这个方式大大提升了权限系统的灵活性和可维护性。
遇到的问题与解决过程

问题一:登录接口频繁超时
我们在压力测试时发现登录接口偶尔会卡住甚至报错。经过排查,发现是加密算法造成的性能瓶颈。
我们用的是 BCryptPasswordEncoder 默认强度,这个值越大加密越安全,但也越慢。我们调整了加密参数:
@Bean
public PasswordEncoder passwordEncoder() {
int strength = 10; // 默认是 12,在保证安全的同时提升性能
return new BCryptPasswordEncoder(strength);
}
同时把登录接口独立部署,避免影响其他接口响应。
问题二:Token 刷新机制缺失
JWT 默认不支持刷新机制,一旦签发就只能等过期。这对于用户体验很不友好。所以我们设计了一个 Token 刷新接口,配合 Refresh Token:
- 登录成功返回 Access Token 和 Refresh Token
- 当 Access Token 即将过期时,客户端调用
/refresh接口换取新的 Token - Refresh Token 存储在 Redis 中,可设置过期时间(比如 7 天)
这使得安全性、体验性都得到了提升。
实际上线后的效果
这套认证系统上线后,给团队带来了不少好处:
- 所有服务统一了认证流程,减少了重复开发
- 通过 Gateway 做前置鉴权,节省了大量无效请求处理资源
- 权限控制灵活可配,能快速响应新业务的权限需求
- 集中化管理用户行为日志,方便后续审计追踪
最重要的一点是:我们不再需要为每个业务系统单独维护一套认证模块了。
我的经验总结
作为一个经历过多次安全模块重构的开发者,我想分享几点建议给正在使用 Spring Security 的你:
✅ 推荐实践
- 优先使用 Spring Security 提供的默认安全机制,不要盲目自研
- 合理配置过滤器链路,避免拦截错误或漏检
- 使用 JWT + Redis 做无状态认证,适合分布式系统
- 采用方法级权限控制,而不是粗暴的角色匹配
- 把权限数据集中管理,提升扩展性和易维护性
❌ 避免踩坑
- 不要硬编码权限信息到代码中,不利于运营和修改
- 不要忽略 Token 续期机制,否则会导致频繁登录
- 不要低估加密算法对性能的影响,尤其是高频登录接口
- 不要在 Filter 中做耗时操作,容易成为性能瓶颈
最后聊聊我对认证系统的思考
Spring Security 不只是个“安全框架”,更像是一个安全平台的基础骨架。它提供了强大的钩子和扩展能力,但需要你深入理解其内部机制才能真正用好它。
在我们团队中,大家现在都已经习惯把认证授权当成系统标配的一部分来看待,不再觉得这是个附加功能。相反,它是保障整个系统安全性的基石。
如果你现在正打算引入 Spring Security,不妨从一个最小可行性系统做起,然后逐步加上权限控制、审计记录、多租户等功能。记住一句话:安全不是堆叠功能,而是贯穿始终的设计理念。
这篇文章写到这里也算差不多告一段落了。希望它能帮助你在搭建安全系统的过程中少走弯路,少踩坑。如果对你有帮助,欢迎留言交流;如果文中有什么讲得不够清楚的地方,也欢迎指正。我们一起进步 🙌
文章作者:一名热爱 Java 的后端工程师,在互联网行业摸爬滚打了五年,参与过多个企业级安全系统的建设与重构。

评论 0