从0开始用Spring Security构建安全认证系统:我的踩坑与成长之路
开头:为什么要写这篇博客?

作为一名工作5年的后端开发工程师,我经历过从传统单体架构到微服务、再到如今的云原生时代的变迁。在各种项目中,有一个模块几乎是每个系统都绕不开的——用户权限控制和安全认证模块。
记得刚入行时,我负责的第一个任务就是“做一个登录接口”。当时我觉得这事儿简单极了——校验用户名密码,返回个 token 就完事了。然而现实给了我当头一棒,不仅有权限分级、token 刷新机制的问题,还因为没有做好安全设计导致系统被攻击……那一次失败让我彻底认识到:安全不是功能,而是一种责任。
从那时起,我开始认真学习 Spring Security,也慢慢把它变成自己后端技术栈中的“标配组件”。今天我想结合几个真实项目的经历,分享一下我是如何一步步用 Spring Security 快速搭建出一个稳定、灵活且可扩展的安全认证系统。
项目背景:一个内容管理平台的身份认证需求

去年我们团队接到一个新的项目——给一家教育机构搭建一个内容管理系统(CMS),主要供内部编辑和运营使用,用于发布课程、管理文章资源等内容。由于这是面向内部员工使用的系统,所以用户数量不算太大(千级以内),但安全性和操作日志要求非常高。
我们的项目目标之一是:
搭建一个基于RBAC(基于角色的访问控制)模型的安全认证系统,支持多角色、细粒度的接口权限控制,并能集成审计日志。
遇到的挑战:权限模型复杂 + 扩展性差的传统做法

一开始,我们尝试用传统的“手动拦截器”来实现权限控制。比如通过自定义注解 + AOP 的方式做权限判断:
@Permission("article:read")
public ResponseEntity<?> getArticle(Long id) {
// ...
}
这种方式初期看起来没什么问题,但随着业务增长,接口越来越多,权限规则越来越复杂:
- 接口之间的组合权限怎么处理?
- 权限规则修改频繁,每次都要改代码重新上线?
- 不同的角色需要继承不同的权限,怎么设计数据结构?
- 缺乏统一的安全策略配置?
这些问题让我们意识到,手写的权限验证已经难以为继。我们迫切需要一套标准、成熟、易于扩展的安全框架来支撑整个系统的安全性。
于是,我们决定引入 Spring Security + JWT 构建完整认证授权体系。
解决方案:用Spring Security构建安全认证层
最终我们采用的技术栈如下:
- Spring Boot 2.7.x
- Spring Security 5.7.x
- JWT + Redis 实现无状态会话管理
- 使用数据库存储用户角色、权限信息
- 基于方法级别的细粒度权限控制
这个方案的目标是做到以下几点:
- 支持多角色权限分配,动态管理权限;
- 接口级权限控制,包括方法级别;
- 支持权限继承,提高灵活性;
- 登录过程安全可靠(防暴力破解、密码加密等);
- 支持审计日志记录用户行为。
下面我以实际项目为例,介绍一下搭建的核心流程。
实践细节:Spring Security认证授权是怎么落地的?

1. 安全配置类的基本搭建
最基础的入口当然是 SecurityConfig 配置类。这里我们需要重写 configure(HttpSecurity http) 方法:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
}
重点说明:
prePostEnabled = true是为了支持我们在 Controller 中使用@PreAuthorize("@permission.check('xxx')")这种注解;- 使用了 JWT 和 Redis 管理会话,所以启用了 STATELESS 模式;
- 自定义过滤器
JwtAuthFilter会在后面详细解释。
2. JWT身份验证流程设计
我们将用户的登录信息通过 JWT Token 传输,避免服务端保存会话状态,适合分布式部署。Token 主要包含以下内容:
- 用户ID(userId)
- 角色列表(roleIds)
- 过期时间(exp)
登录成功后返回类似这样的 Token:
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMjMiLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImV4cCI6MTcxODA4NTEyNH0.qQfT...
验证逻辑放在自定义的 JwtAuthFilter 过滤器中,大致逻辑如下:
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && jwtService.validateToken(token)) {
String userId = jwtService.getUserIdFromToken(token);
List<String> roles = jwtService.getRolesFromToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userId, null, roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
3. 权限控制:从静态走向动态
最初我们尝试的是硬编码权限检查:
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/page")
public String adminPage() {
return "admin";
}
这种做法在权限较少时还能应付,但一旦权限数量上来就很难维护。后来我们采用了动态权限控制:
- 把所有权限名称存在数据库中(如:user:create,course:delete)
- 在启动时加载权限表至内存缓存(Redis 或本地 HashMap)
- 使用自定义的
PermissionService来进行运行时权限检查:
@Component("permission")
public class PermissionService {
public boolean check(String permission) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
return authorities.stream().anyMatch(a -> a.getAuthority().equals(permission));
}
}
然后在 Controller 中这样使用:
@PreAuthorize("@permission.check('content:publish')")
@PostMapping("/publish")
public ResponseEntity<?> publishContent(@RequestBody ArticleDTO dto) {
// 发布逻辑
}
这样一来,权限就可以由管理员在后台动态配置,前端根据当前用户拥有的权限控制菜单显示,整个系统变得更灵活。
踩过的坑 & 经验总结
1. CORS 和 OPTIONS 请求搞不定?
刚开始整合前后端分离之后,出现了大量跨域请求报错,特别是 /login 接口。
解决办法其实很简单,在 SecurityConfig 中加入如下配置即可允许 OPTIONS 请求放行:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return source;
}
2. 自定义异常无法全局捕获?
一开始我们自定义了多种认证异常,希望通过 @ControllerAdvice 全局处理,却发现有些异常跳过了异常处理器。
原因在于,Spring Security 的过滤链抛出的异常不经过 controller 层,自然也无法触发全局异常处理。解决办法是自定义一个认证异常处理类并加入到过滤链中:
@Component
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.UNAUTHORIZED.value());
result.put("message", "未授权:" + authException.getMessage());
new ObjectMapper().writeValue(response.getOutputStream(), result);
}
}
并在 SecurityConfig 中注册它:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthExceptionEntryPoint exceptionHandler) throws Exception {
return http
.exceptionHandling().authenticationEntryPoint(exceptionHandler)
.and()
// ...其他配置
.build();
}
3. Redis缓存失效导致token无效?
我们在早期使用 Redis 缓存 JWT 的黑名单(用来快速吊销 token),但某天运维同学执行了 flushall……
结果,一些用户明明还在登录状态,却突然被踢下线。为此我们进行了优化:
- 引入 TTL 机制,设置合理的 token 生命周期(例如1小时+刷新机制)
- 黑名单只记录短期强制下线的情况
- 增加登录态自动续期机制,防止频繁掉线影响体验
效果与收获:不只是“做了个认证系统”
经过这套架构的实施后,效果还是非常明显的:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 安全漏洞 | 月均1~2次 | 0次 |
| 接口权限变更效率 | 至少1次代码修改、发版 | 后台动态更新 |
| 权限复用率 | 几乎无复用 | 支持角色继承、组合 |
| 用户并发测试响应时间 | ~300ms | ~120ms(去掉 DB 查询) |
更重要的是,团队内的后端开发者们开始统一使用这一套认证授权体系,大家的开发效率也提升了。
现在我们再接新项目时,只要引入一个 starter 模块,就能完成大部分的权限相关配置,真正做到了“开箱即用”。
给新手的一些建议
- 别一开始就追求完美:如果你只是做个小型项目,可以先用默认的
UserDetailsService+ 内存账号试试; - 了解原理比死记API更重要:比如理解 Filter Chain、AuthenticationManager 的作用;
- 不要忽视性能问题:权限校验每秒可能被执行上千次,尽量减少 DB 查询频率;
- 安全是一场持久战:哪怕是一个小系统,也要养成良好的安全意识;
- 善用社区工具:Spring Security 社区强大,很多问题已经有最佳实践,没必要重复造轮子;
- 保持学习和思考:OAuth2、JWT、SAML2 这些都在演进,保持对新技术的好奇心很重要。
最后的话:写给正在努力的你
说实话,我在最初学习 Spring Security 的时候也非常懵圈。文档又长又枯燥,网上搜出来的教程又太理想化,根本不知道在生产环境怎么用。
所以我写下这篇文章的目的,不仅是教你“怎么搭”,更是想告诉你,“为什么这么搭”,以及“出了问题怎么办”。
希望这篇文章对你有所帮助。如果文中有什么地方描述得还不够清楚,欢迎留言交流~我也很乐意继续深入探讨这个问题。
祝你早日成为真正的「安全后端工程师」!💪
参考链接:
- Spring Security 官方文档
- JWT官网
- 《Spring Security实战》 by 陈晋华

评论 0