Spring Security基础:快速搭建安全认证系统
从零到一:Spring Security实战搭建安全认证系统

去年我参与了一个企业级 SaaS 系统的重构项目,其中一个核心诉求就是“用户访问控制”必须做得既安全又灵活。当时我们团队的技术栈已经是 Spring Boot,所以顺理成章地选用了 Spring Security 作为安全框架的核心组件。
说实话,在这之前我对 Spring Security 的理解还停留在 “登录拦截” 和 “权限配置” 的表层,真正开始深入研究后才发现它的复杂程度远超想象,同时也非常强大——它不仅覆盖了从登录验证、权限管理、CSRF 防御等传统安全场景,还能通过扩展支持 OAuth2、JWT、动态权限策略等多种现代方案。
今天这篇文章我想用真实项目中的一个小模块来和你分享:我们是如何一步步使用 Spring Security 搭建一个基本但功能完整的安全认证系统的,并且过程中遇到的那些典型“坑”,我也毫无保留地分享出来。
背景:为什么需要重新设计安全系统?
这个 SaaS 系统原本有一个自定义的身份认证模块。由于早期为了快速上线赶进度,很多逻辑是硬编码在业务代码里的,比如:
if ("admin".equals(username) && "123456".equals(password)) {
// 登录成功
}
随着用户规模增长和多角色体系的引入(普通用户、管理员、审计员等),这种简单粗暴的方式带来的问题逐渐暴露:
- 密码存储不安全,明文保存在数据库中;
- 身份判断逻辑分散,修改一处权限要动多个文件;
- 扩展性差,没有统一的安全策略抽象层;
- 缺乏日志与监控机制,异常行为难以追踪;
- 无外部集成能力,未来想接入微信授权或钉钉认证根本做不到。
因此,我们决定重构整个安全认证系统,目标是实现:
- 用户账户密码安全存储;
- 支持 RBAC(基于角色的访问控制)模型;
- 提供对外接口鉴权能力;
- 可扩展性强,方便对接第三方平台认证;
- 异常登录行为可记录并触发告警;
- 接入性能要稳定,不影响整体响应时间。
我们最终选择的是 Spring Boot + Spring Security 的组合,结合 JWT 做 Token 认证,外加 Redis 存储会话信息。这套架构在我们的系统中已经跑了快一年,效果很好。
实施过程:Spring Security 初体验踩坑记
第一步:引入依赖,初始化基础结构
我们使用的 Spring Boot 版本是 2.7.x,对应的 Spring Security 是 5.7.x,Maven 配置如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加完依赖之后,启动应用会发现所有接口都被拦截了,默认的登录页也激活了。这说明 Spring Security 已经生效。
但这远远不够。我们需要定制自己的登录流程、权限校验方式,还需要跳过部分开放接口(如 /login, /register)的拦截。
为此,我们创建了一个继承 WebSecurityConfigurerAdapter 的类进行配置(注意:如果你使用的是 Spring Boot 2.7+,此方式依然有效;如果使用更高版本,请改用基于组件注册的方式):
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/register").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and()
.logout()
.logoutUrl("/logout")
.clearAuthentication(true)
.invalidateHttpSession(true);
}
}
这里有几个关键点要提一下:
formLogin()不仅提供了默认登录页,还可以支持前端发起登录请求(POST /login);- 自定义的成功/失败处理器可以做日志记录或重定向;
- 退出登录时一定要记得清除 Session 和 Authentication。
不过,这时候我们的用户信息还是从哪来的?这就涉及到另一个核心点:如何加载用户信息。
第二步:用户信息加载机制 —— UserDetailsService
Spring Security 中负责用户信息获取的核心接口是 UserDetailsService,我们要实现它的 loadUserByUsername(String username) 方法:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
注意几点:
- 返回的对象是
UserDetails类型,Spring Security 内部会用它来做比对; - 权限字段需要以
ROLE_开头才能被自动识别为“角色”; - 密码要经过加密处理后再传入构造函数,否则会出现“Bad credentials”。
说到加密,我们就不得不提下一个重点模块:
第三步:密码加密策略(PasswordEncoder)
Spring Security 提供多种密码加密方案,最推荐的是 BCryptPasswordEncoder,因为它每次加密结果都不一样,安全性高。
我们在配置类中定义 Bean:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
并在注册时调用:
String rawPassword = "user123";
String encodedPass = passwordEncoder.encode(rawPassword);
这样一来,存储到数据库中的就是加密后的字符串,即便泄露也不会立刻被破解。
不过有个小插曲我得说下:最初我们把加密算法写到了 Service 层之外,导致测试环境用明文,线上加密,结果在登录的时候出现匹配失败,排查了很久才发现这个问题。
从此以后我们都约定,一切与密码相关的行为都应在服务层处理,不允许透出给 Controller 或其他模块。
第四步:实现 Token 登录 —— 结合 JWT
前面讲的是传统的 Cookie + Session 登录模式。但在前后端分离架构中,我们更倾向于使用 Token 机制,例如 JWT。
为了让 Spring Security 支持 Token 登录,我们做了以下几步:
- 创建 JWT 工具类生成和解析 Token;
- 自定义 Filter,拦截
/login请求并生成 Token; - 在后续请求中加入 Token 校验逻辑;
- 修改
SecurityConfig配置放行静态资源和公共接口。
这部分代码相对较多,但我挑选两个核心部分展示:
生成 Token(简化版)
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
.signWith(SignatureAlgorithm.HS512, JWT_SECRET)
.compact();
}
自定义 Token 过滤器(继承 OncePerRequestFilter)

public class JwtAuthenticationTokenFilter 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 = extractUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
然后把这个 Filter 注册进 Spring Security 的链路中:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new JwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
这样就实现了 Token 登录的基础逻辑。
项目落地中的一些实际经验
数据库设计考虑
用户的权限体系我们采用的是 RBAC(基于角色的访问控制)模型,涉及三张主要表:
user:用户基本信息;role:角色表;user_role:用户角色关联表;permission:权限项(如 read, write);role_permission:角色权限关联表;
这种设计的好处是易于扩展。如果我们后期想要增加权限级别的细粒度控制(比如按数据范围控制),只需修改 PermissionEvaluator 即可。
性能优化要点
- 避免频繁查询数据库:我们使用 Redis 缓存 Token 对应的用户信息,提升登录态识别速度;
- 减少同步锁竞争:在并发环境中,Token 解析要尽可能无状态,不要依赖共享变量;
- 日志记录适度化:安全相关的事件要记录,但不能因为记录影响主流程性能;
- 启用缓存过滤:针对
/login接口加上滑动验证码或 IP 限流策略,防止暴力破解。
日志与安全监控
除了基本的日志记录,我们还做了几件事:
- 将登录失败次数写入 Redis 并设置短过期时间,达到上限时临时锁定账户;
- 所有登录成功/失败行为都打到日志文件,配合 ELK 进行分析;
- 设置定时任务扫描异常 IP 行为,触发告警;
- 审计敏感操作(如修改密码、删除账号)需记录上下文信息。
这些措施大大增强了系统的防御能力。
那些年我们踩过的坑
总结几个我在项目中亲自趟过的雷:
| 问题描述 | 原因 | 解决方法 |
|---|---|---|
| 登录时提示“Bad credentials”,但用户名密码正确 | 忽略了密码加密 | 检查是否在登录前对输入密码进行了加密 |
| 角色不生效,无法访问受限页面 | Role 名称没加 ROLE_ 前缀 |
规范命名规则,或者在配置中指定角色前缀 |
| CORS 问题导致前端无法访问接口 | 后端未开启跨域支持 | 使用 @CrossOrigin 注解或配置全局 CORS |
| 多个 Filter 执行顺序混乱 | Filter 添加位置不准确 | 使用 http.addFilterBefore() 明确顺序 |
| 退出后仍能访问受限接口 | Session 未清除干净 | 设置 .clearAuthentication(true) 并注销 Session |
| Token 登录失效频繁 | Expire 时间设置太短 | 动态刷新 Token 或延长有效期 |
还有一次,我错误地在 UserDetailsService 的返回对象中设置了空的角色列表,结果导致所有接口都无法访问,调试了整整两个小时才定位到问题 😅
最终成果与收益
现在我们的安全模块已经在生产环境中稳定运行了一年多,支撑着超过 10 万用户的日常使用。
具体收益如下:
- 所有用户密码均已加密存储,符合基本安全合规要求;
- 支持前后端分离架构下的 Token 登录和自动刷新;
- 多角色权限体系清晰,便于维护和扩展;
- 出现异常行为可快速定位溯源,降低安全风险;
- 第三方系统可通过 OAuth2 协议无缝集成;
- 整体性能稳定,平均每秒处理 200+ 登录请求没问题。
而且最重要的一点:当我们准备接入公司内部的统一认证中心时,只需新增一个适配模块即可完成整合,原有架构无需大改。
给开发者的建议与思考
如果你正在尝试使用 Spring Security 或者打算重构现有的安全机制,我的一些实践经验或许对你有用:
- 别急着搞复杂的功能。先从简单的账号密码登录做起,了解 Spring Security 的基本流程;
- 重视安全合规规范。即使只是个中小型项目,也要避免明文密码和简单加密;
- 权限设计要有扩展性。RBAC 是一个成熟模型,值得在大部分项目中使用;
- 尽早介入日志与审计设计。安全事件的回溯非常重要,否则出问题时你会后悔没早点准备;
- 关注社区生态和技术趋势。比如 Spring Security 新版本对 OAuth2 与 OpenID Connect 的支持已经越来越完善,适合考虑长期演进的系统接入;
- 适当封装公共模块。将 Token 工具类、Redis 操作类、权限注解等做成通用组件,提高复用率。

另外,如果你是刚接触 Spring Security 的同学,建议从官方文档和 GitHub 示例入手。虽然文档有点冗长,但它几乎涵盖了你能想到的所有使用场景。
写在最后:关于技术成长的思考
其实,安全认证这类技术模块,往往是很多人忽略的地方。大家更多时候关注的是业务代码、接口设计、性能优化,却很容易忽视系统的入口防护。
而一旦出现疏漏,可能就是一场灾难。
所以在我看来,一个好的架构师或者工程师,不仅要懂怎么写出漂亮的业务代码,还要具备“构建防线”的意识。
Spring Security 就像是一把钥匙,打开了通向现代安全体系的大门。掌握了它,不只是让你学会了个工具,更是让你站在巨人的肩膀上,去应对更加复杂的工程挑战。
希望这篇基于真实项目的分享能够帮到你,也欢迎留言交流你的经验和想法。
一起成长,一起变强 😊

评论 0