Spring Security基础:快速搭建安全认证系统的实战经验分享
大家好,我是有着五年后端开发经验的一线工程师。从最初的Spring Boot初学者到如今负责一个中大型项目的架构设计,Spring Security一直是我在构建系统安全方面的重要工具。今天想和大家分享一下我在这个过程中的一些经验,特别是围绕“如何快速搭建一个安全认证系统”这个主题。
这篇文章不会堆砌一堆理论知识,而是会结合我自己的项目经历,带你看清实际落地的Spring Security应用到底是怎么跑起来的。尤其是当你面对的是一个需要在短时间内上线的安全系统时,这套框架能帮你解决很多问题。
背景介绍:为什么选择Spring Security?


记得去年我参与了一个公司内部的服务平台重构项目。这个平台原本是基于传统的Servlet+Filter模式实现的权限控制,但随着用户量的增长和业务逻辑的复杂化,老系统在安全性和扩展性上都暴露出很多问题。比如:
- 用户登录信息容易被伪造;
- 权限分配粒度粗,难以管理;
- 没有统一的接口鉴权机制,导致某些API可以绕过登录访问;
- 登录后的 Session 保存方式不统一,跨服务间共享困难;
- 系统维护成本高,每次改权限都需要手动改代码、重启服务。
当时我们决定重构整个身份认证模块,并引入更现代化的安全框架来支撑后续的微服务拆分。考虑到团队的技术栈主要是 Java + Spring Boot,所以自然把目光锁定在 Spring Security 这个官方推荐的安全框架上。
遇到的挑战:不是所有文档都能解决问题

虽然 Spring Security 官方文档很详尽,但真正用起来却发现文档和实践之间还是有不小的差距。尤其是在以下这些方面遇到了不少坑:
- 多种安全策略并存的情况下怎么配置?
- 如何优雅地集成 JWT 做无状态认证?
- 自定义的 UserDetailsService 和 UserDetails 实现总出错?
- CSRF、CORS、Session管理等概念理解不清时容易配崩。
- 如何与数据库交互完成真正的用户认证?
特别是在早期版本中,Spring Security 的 API 变动频繁,有时候按照网上的教程写完之后发现根本不起作用,还要反复 debug 才知道是新旧版本的写法变了。
这些问题让我意识到,光看文档远远不够,必须深入理解其背后的原理和流程,才能在不同场景中灵活使用。
解决方案:一步步搭建安全认证核心模块

整个系统的安全模块目标很明确:支持多角色权限体系,实现基于 Token(JWT)的无状态鉴权,同时兼顾传统 Cookie Session 的兼容支持,最终做到可插拔、可扩展。
技术选型和架构设计
我们在安全模块的设计上采用了如下技术组合:
- Spring Boot 2.7 + Spring Security 5.7:选择相对稳定的新版本,支持 OIDC、OAuth2 和较完整的 JWT 支持。
- JWT + Redis:用于 Token 的生成与存储,Redis 缓存用户的在线状态,避免频繁访问 DB。
- MySQL 数据库存储用户信息、角色、权限表:采用关系型结构,便于维护。
- MyBatis Plus + Druid:操作数据库,配合 Druid 监控 SQL 性能。
- Logback + MDC 日志追踪:记录安全相关操作日志,方便后期审计。
整体架构图简化如下:
[客户端] --> [Nginx/网关层] --> [业务模块]
|
[安全认证中心]
/ | \
[JWT签发] [Session管理] [权限校验]
其中的安全认证中心负责处理所有身份验证、授权和令牌发放的工作。
核心流程梳理
为了让大家有个清晰的认知,这里简单画了一个用户登录流程的时序图:
- 用户通过
/login接口提交账号密码; - 后端使用
AuthenticationManager.authenticate(...)去验证; - 自定义的
UserDetailsService从数据库加载用户数据; - 密码匹配成功后,生成 JWT token 并返回给前端;
- 前端将 token 放入请求头(Authorization: Bearer xxx),后续请求携带该 token;
- 拦截器通过解析 JWT 获取用户信息,交给 Spring Security 上下文,完成鉴权;
- 访问受保护的资源(如
/api/admin/user-list)时由@PreAuthorize("hasRole('ADMIN')")等注解拦截并校验权限。
接下来我们会看看这一套流程是怎么通过 Spring Security 快速搭出来的。
代码实践:关键组件实现思路详解
下面我会展示几个关键部分的实现,包括配置类、自定义过滤器、Token 工具类等。
1. 安全配置主类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final AccessDeniedHandler accessDeniedHandler;
private final AuthenticationEntryPoint authenticationEntryPoint;
public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter,
AccessDeniedHandler accessDeniedHandler,
AuthenticationEntryPoint authenticationEntryPoint) {
this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
this.accessDeniedHandler = accessDeniedHandler;
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.build();
}
// 其他 Bean 如 passwordEncoder、AuthenticationManager 略
}
这段配置干了这么几件事:
- 关闭 CSRF:因为我们使用的是 JWT,不需要防范 CSRF;
- 设置为无状态 Session:不再依赖容器的 Session;
- 添加自定义 JWT 过滤器;
- 处理异常情况:没有权限访问或未登录的情况,分别由两个处理器处理;
- 设置访问控制规则。
📌 小提示:如果你的应用前后端同源部署,可能仍需开启 CORS 或者适当调整配置,防止跨域请求被安全机制拦下。
2. 自定义 JWT 过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsServiceImpl userDetailsService;
public JwtAuthenticationTokenFilter(JwtService jwtService, UserDetailsServiceImpl userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && jwtService.validateToken(token)) {
String username = jwtService.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
这一步是最关键的安全拦截点。它会:
- 从 Header 中提取 Token;
- 验证 Token 是否有效;
- 如果有效,则去数据库加载用户详情;
- 创建
Authentication对象,并塞进当前线程上下文中,供后续鉴权使用。
💡 个人建议:不要在这里做数据库查询太多次,可以在
JwtService中缓存一部分用户信息,减少压力。
3. 使用 @PreAuthorize 控制接口权限
一旦你配置好了 @EnableGlobalMethodSecurity(prePostEnabled = true),就可以在 Controller 上直接使用如下注解:
@GetMapping("/users")
@PreAuthorize("hasAuthority('user:read')")
public List<User> getAllUsers() {
return userService.findAll();
}
这种方式非常直观,也符合实际的权限设计需求。当然,你也可以使用 hasRole() 来判断用户是否具备某个角色权限。
不过有一点需要注意:Spring Security 默认的权限前缀是 ROLE_,如果你在数据库里存的角色名没有带上这个前缀,比如叫 "ADMIN",那么你要么加一个自定义的 GrantedAuthority,要么使用如下形式:
@PreAuthorize("hasRole('ADMIN')")
然后确保你的 UserDetails 返回的 authorities 是带有 ROLE_ 前缀的。
4. 登录接口实现
登录接口的核心在于调用 Spring Security 的 AuthenticationManager.authenticate(...) 方法:
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtService.generateToken(authentication);
return ResponseEntity.ok().header("Authorization", "Bearer " + token).build();
}
这段代码背后触发了几个关键流程:
- Spring Security 内部找到匹配的
AuthenticationProvider,一般是DaoAuthenticationProvider; - 提取用户名密码,调用
UserDetailsService加载用户; - 匹配输入密码与数据库中的加密后的值(通常使用 BCryptPasswordEncoder);
- 如果通过认证,则构建一个经过身份验证的
Authentication实例; - 最后我们拿着这个实例生成 JWT Token 返回。
⚠️ 注意点:默认情况下,如果认证失败,Spring Security 会抛出异常,而不是返回 JSON 错误。因此你需要统一捕获
AuthenticationException并封装成 JSON Response 返回。
踩坑经验分享:那些让你夜不能寐的问题

下面我想分享几个在实际工作中遇到的真实问题及解决方案:
1. CORS 与 OPTIONS 请求被拦截
我们初期在前端使用 Vue 调用接口的时候,总是出现预检请求失败的问题。后来发现是因为没有放行 OPTIONS 请求,而且 Spring Security 的顺序配置不当也会造成问题。
解决方案:
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
...
此外,还可以在全局配置中显式允许跨域:
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
再将其添加到 Spring Security 的过滤链前面:
.addFilterBefore(corsFilter(), WebAsyncManagerIntegrationFilter.class)
这样就解决了跨域问题。
2. JWT 被篡改但仍然通过验证
有一次测试环境发现,有些非法 Token 竟然也能通过验证!后来排查发现是因为签名密钥太简单或者没正确传递。
建议:
- 使用长度足够的随机字符串作为密钥;
- 在生成和验证时保持一致;
- 不要硬编码密钥,可以通过配置文件注入;
- 推荐使用
io.jsonwebtoken提供的库来做 JWT 的签发与校验。
3. 多种认证方式混用时搞乱过滤链顺序
一开始我尝试在一个系统中同时支持 JWT 与 Session 登录,结果两种方式互相干扰。
后来通过将两者分离为不同的子路径(比如 /api/v1/auth/* 下使用 JWT,其他路径使用 Session 登录),并通过多个 SecurityFilterChain 分别配置才解决了这个问题。
@Bean
@Order(1)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/v1/**")
...
.build();
}
@Bean
@Order(2)
public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.formLogin()
...
.build();
}
这种结构让不同路径走不通的安全链路成为可能。
效果总结:系统上线后的收益
这套基于 Spring Security 的安全认证系统上线后,我们的系统在以下几个方面得到了显著提升:
- 安全性提升:统一的身份认证机制,防止越权访问;
- 开发效率提高:权限控制变得可视化,修改只需配置注解;
- 接口响应速度变快:避免了重复的权限校验逻辑;
- 运维更方便:权限变动无需重新部署服务;
- 扩展性强:未来接入 OAuth2、SSO 等更加容易。
更重要的是,系统上线后几乎没有因权限问题引发的重大线上故障。这说明这套方案不仅满足了功能需求,也具备良好的稳定性和健壮性。
经验总结与建议
通过这一次对 Spring Security 的深入实践,我也积累了一些心得体会,在这里分享给大家:
✅ 推荐的做法:
- 使用
@PreAuthorize做方法级权限控制,比在业务层硬编码更清晰; - 使用 JWT 做无状态认证,适合 RESTful API 场景;
- 将权限分为 ROLE + AUTHORITY 两级,方便管理;
- 引入 Redis 缓存在线用户列表,实现强制登出;
- 结合日志系统跟踪安全事件,方便后期审计。
❌ 应该避免的地方:
- 不要在拦截器或过滤器中频繁访问数据库;
- 避免在 Token 中存放敏感信息;
- 不要忽视 Session 的生命周期管理;
- 避免混合使用多种安全机制而不划分路径;
- 不要随意关闭 CSRF,除非你真的不需要。
👨💻 给新手的一点建议:
如果你是刚开始接触 Spring Security 的同学,我的建议是:
- 先理解整个流程图,比如 Authentication 是怎么一步一步走到最后的;
- 自己动手写一个最小可运行的安全工程,哪怕是只保护一个接口;
- 遇到问题不要怕折腾,调试是最好的老师;
- 结合日志去看流程,很多时候问题就是出现在某一个 filter 没被触发;
- 多查 Spring Security 的 issue 列表或社区讨论,说不定别人早就踩过同样的坑。
写在最后:技术的本质是为了创造价值
其实这篇文章写到这里,我已经不止一次回想起那段时间和队友们一起调试、修 Bug、查日志的日子。虽然过程有点煎熬,但当我看到系统稳定上线、权限逻辑井井有条时,心里那种踏实感是真实的。
Spring Security 不只是一个框架,它更像是我们守护系统安全的一道坚固防线。而作为一名后端工程师,能够熟练掌握这门技术,就意味着你能为团队带来更高的安全保障和开发效率。
希望这篇来自真实工作场景的经验分享,对你有所帮助。也欢迎你在评论区交流你在实际项目中遇到的安全问题,我们可以一起探讨更好的解决方案。
如果你觉得这篇文章对你有用,欢迎点赞、收藏、转发。也欢迎关注我的公众号【Java成长日记】,一起交流后端技术的成长之路。

评论 0