从零开始搭建安全认证系统:我的 Spring Security 实战经验
引子:项目背景与初衷

去年我们团队接了个新项目,是一个企业级的管理系统,涉及到员工信息、考勤打卡、薪资计算这些敏感模块。客户特别强调了数据的安全性,明确提出需要一套完整的权限管理机制,用户必须经过身份验证才能访问系统功能。
一开始,我在技术选型上其实也有过纠结,是继续用 Shiro 还是换成 Spring Security?毕竟公司之前几个项目都是用的 Apache Shiro,大家都熟悉,文档也齐全。但考虑到 Spring Boot 几乎成了标配,而 Spring Security 和 Spring Boot 的整合更顺畅,加上现在主流的 OAuth2、JWT 等安全协议支持得也更好,最终我决定尝试在项目中引入 Spring Security。
这篇文章记录的是整个项目搭建安全认证系统的过程,包括踩过的坑和一些关键点的实现思路。如果你也在做类似的工作,或者准备入门 Spring Security,希望这篇分享能让你少走些弯路。
遇到的问题与挑战

1. 复杂权限需求 vs 安全框架的适应性
客户提的需求不少,比如:
- 用户登录后要根据角色展示不同的菜单;
- 每个接口都有明确的权限控制,不能越权访问;
- 不同角色之间权限可以灵活配置;
- 要求支持前后端分离的 JWT token 登录方式;
- 登录失败要限制次数,防止爆破攻击;
- 登录成功后记录日志,方便审计。
这些功能听起来都挺常见的,但在一个刚起步的项目里同时满足所有要求,难度还是不小的。尤其是如何把 Spring Security 的权限模型跟我们自己的 RBAC(基于角色的权限控制)模型对接好,这成了初期最大的痛点。
2. 多种认证方式共存问题
项目早期我们只做了表单登录,但随着前后端分离架构落地,前端不再依赖模板渲染,而是完全独立的 Vue 前端应用。这就意味着我们需要支持两种认证方式:
- 传统的
formLogin表单登录(主要用于后台管理员) - JWT Token 的无状态登录(用于前后端分离场景)
两种认证机制在 Spring Security 中并行工作,导致我们在 Filter 配置和异常处理上频频踩坑。
3. 第三方集成和扩展成本高
后来客户又提出,希望未来能接入 LDAP、OAuth2 第三方登录等方式。虽然 Spring Security 提供了一些扩展点,但在实际使用中发现很多细节不透明,文档不够清晰,光是查阅资料就耽误了不少时间。
解决方案:Spring Security + 自定义策略

技术选型
我们的整体技术栈是:
- 后端:Spring Boot + Spring Security
- 数据库:MySQL + MyBatis Plus
- 前端:Vue3 + Element UI
- 认证方式:Form Login + JWT Token
- 权限管理:RBAC 模型 + 动态权限过滤
为了兼容传统登录方式和前后端分离,我们采用了双认证模式设计。通过两个不同的 Filter Chain 实现不同路径的安全策略。
架构设计思路
大致结构如下:
Browser Mobile App Third Party APIs
| | |
[Form Login] [JWT Auth] [OAuth2 / SSO]
\ / \
\ / \
↓ ↓ ↓
Spring Security (多FilterChain模式)
↓
RBAC权限验证
↓
接口业务逻辑
在 Spring Security 的配置中,我们主要做了这几件事:
- 划分 URL 权限等级:将
/admin/**划分为需要登录且有角色的角色访问,/api/**使用 JWT Token 认证。 - 自定义 UserDetailsService:对接数据库中的用户信息。
- JWT Token 支持:在请求头中解析 Token,进行鉴权。
- 动态权限控制:实现可配置的接口级别权限管理。
- 登录失败处理 + 日志记录:增强系统的可观测性和安全性。
关键代码实践
下面我会以一个最小可运行的例子展示核心代码结构,省略部分配置,保留最核心的部分。
1. 自定义 UserDetails 实现类
public class CustomUserDetails implements UserDetails {
private String username;
private String password;
private boolean accountNonExpired = true;
private boolean credentialsNonExpired = true;
private boolean accountNonLocked = true;
private Collection<? extends GrantedAuthority> authorities;
// get/set methods omitted
}
2. 实现 UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userService.getByUsername(username);
if (sysUser == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return new CustomUserDetails(sysUser);
}
}
3. 配置 SecurityConfig(简化版)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Bean
public SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/admin/home")
.failureUrl("/login?error=true")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login");
return http.build();
}
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**")
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests()
.anyRequest().authenticated();
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(customUserDetailsService);
provider.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
return provider;
}
}
4. JWT 认证 Filter 示例
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)) {
Authentication auth = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
// 提取Token、校验、生成Authentication等方法略去...
}
踩过的坑与解决思路
1. 多个 FilterChain 冲突问题
刚开始我们用了 @Order 注解来区分 FilterChain 的顺序,结果发现某些请求被放行了,某些明明匹配不到路径却进去了。
解决办法:
- 显式使用
.securityMatcher()来绑定每个 FilterChain 对应的路径; - 使用不同的
http.authorizeRequests()策略; - 避免多个链共享同一个
http.formLogin()配置。
2. 动态权限加载性能问题
一开始我们每次请求接口都要重新查询数据库获取用户的权限列表,这明显会影响性能。尤其在并发量大的时候,接口响应慢了一大截。
优化方案:
- 缓存权限信息:使用 Redis 存储用户角色和权限映射;
- 加入本地缓存(如 Caffeine),避免频繁访问 Redis;
- 增加权限更新通知机制,保证缓存一致性。
3. 登录失败锁定策略实现
虽然 Spring Security 本身没有提供“连续失败多少次后锁定账户”的机制,但我们自己可以在数据库中维护一个 failed_attempts 字段,并结合 Redis 进行实时控制。
实现要点:
- 每次登录失败时更新该字段;
- 达到阈值后记录锁定时间;
- 登录前先检查是否处于锁定状态;
- 使用 Redis 设置滑动窗口限制(例如每小时最多 10 次登录失败)。
4. 跨域(CORS)引发的权限异常
由于前端用的是独立部署的 Vue 应用,跨域问题一度让我们头疼。Spring Security 默认会拦截 OPTIONS 请求,导致前端发起的 POST 请求无法正确触发 CORS 协商。
解决方案:
在 Security 配置中开启跨域支持:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
成果回顾与系统收益
这个项目上线已经一年多了,Spring Security 整体表现非常稳定。通过这套安全体系,我们实现了以下目标:
- 所有用户操作都需通过认证,保障了系统的安全性;
- 权限配置灵活,可以通过界面配置接口级别的访问控制;
- 支持多种认证方式,适应不同的使用场景;
- 系统具备良好的扩展性,为后续接入 SSO、OAuth2 打下了基础;
- 登录日志、失败记录完备,方便后续审计和运维。
更重要的是,整套系统并没有因为增加安全层而导致性能下降。经过压测,在并发量达到 200 QPS 的情况下,认证层平均响应时间仍控制在 2ms 左右。
我的经验建议
如果你正准备在项目中引入 Spring Security 或者正在使用它遇到了问题,我可以给你几个实用的小建议:
1. 不要死磕文档,动手才是王道
Spring Security 的文档内容丰富,但很多时候你真正理解某个机制的方式就是——写个 Demo 跑一遍。
你可以新建一个 Spring Boot 项目,把你想测试的功能写进去,跑起来看看行为是不是符合预期。
2. 分清楚认证与授权的区别
很多人一上来就把 Security 当作“只能用来登录”,其实它的功能远不止于此。
- 认证(Authentication):你是谁?
- 授权(Authorization):你能做什么?
这两部分可以分开设计,甚至由不同的模块负责处理。特别是在 RBAC 场景下,这一点尤为重要。
3. 尽早考虑扩展性
不要一开始就搞得很复杂,但一定要预留扩展空间。比如:
- 接口级别的权限可以抽象成
Permission类; - 用户、角色、权限的关系可以做成关联表;
- 权限校验逻辑抽离出来,便于后期接入 RABC、ABAC 模型;
- 如果以后要考虑多租户、OAuth2,不妨预留适配器接口。
4. 别忽视异常处理和日志追踪
安全相关的错误,比如未认证或权限不足,往往会返回一堆模糊的 401/403 错误码。这对于调试是非常痛苦的。
建议:
- 统一封装 Security 相关的异常输出;
- 在全局异常处理器中捕获
AccessDeniedException、AuthenticationException; - 添加请求上下文的日志追踪 ID(MDC),便于定位问题。
5. 生产环境别忘了安全加固
- 关闭 Debug 页面和默认的
/actuator端点访问权限; - 配置登录失败尝试次数限制;
- 使用 HTTPS 加密传输;
- 定期轮换密钥(特别是 JWT 的 signingKey);
- 开启安全日志审计,对敏感操作记录用户 IP、时间、动作等信息。
结语:安全不是终点,而是起点
作为开发者,我们总是在追求更高的性能、更好的体验、更快的开发效率。但有时候,我们会忽略系统的基本安全防线。
通过这次项目实战,我深刻体会到,一个好的安全系统不是靠“防御”建立起来的,而是通过细致的设计、良好的架构、持续的运维来构建的。
Spring Security 是一个强大而复杂的框架,但它并不是万能的。真正的系统安全,永远离不开我们对业务的理解和对风险的把控。
如果你问我:“Spring Security 难吗?”我会说:“难的是如何把它用得恰到好处。”
最后,送大家一句我常挂在嘴边的话:“代码是冰冷的,架构是有温度的。”愿你在每一次技术选择中,都能找到属于自己的那份“安全感”。
如果觉得这篇文章对你有帮助,欢迎点赞、收藏,也欢迎留言交流你的 Spring Security 实战经验。

评论 0