Spring Security基础:一次真实项目中的快速安全认证搭建
开篇背景:为什么选择Spring Security?

去年年底,我参与了一个企业级SaaS平台的后端重构项目。这个平台面向中小型企业客户提供CRM服务,用户量已经突破10万,并且每天都在快速增长。随着业务发展,原有的权限系统显得愈发捉襟见肘,不仅代码混乱、难以维护,而且安全性存在严重漏洞——比如明文存储密码、越权访问等问题。
项目组决定从零开始重构整个认证和权限模块,同时需要在两周内完成基础功能上线,为后续RBAC模型做铺垫。时间紧迫,架构必须稳定、扩展性要好,我们最终选择了Spring Security作为核心安全框架。虽然之前有接触过Shiro和自己造过轮子,但在企业级项目中使用Spring Security还是头一回。
这篇文章想结合那次实际经历,聊聊我是如何用Spring Security快速搭建起一个基本安全认证系统的,中间踩过的坑,也包括一些个人理解和经验总结。
问题描述:从零到有,我们遇到了哪些挑战?

1. 时间紧任务重
我们需要在两周内把用户登录、注册、Token管理(JWT)、数据库加密等基础功能全部跑起来,不能拖慢主线开发节奏。
2. 安全性和规范性要求高
由于是SaaS产品,客户数据敏感,必须符合基本的安全规范:
- 密码不得明文存储;
- 接口需统一鉴权;
- 登录失败限制机制;
- 防止暴力破解攻击;
- 支持OAuth2(预留);
3. 对接已有系统困难
老系统遗留的数据结构比较杂乱,字段命名风格不统一,迁移过来的接口还要兼容旧逻辑,对权限拦截机制提出了更高要求。
4. 技术选型与学习成本平衡
团队成员中有几位对Spring Security并不熟悉,既要保证实现正确,又要兼顾可维护性和交接文档的编写。
解决方案:技术选型和架构设计思路

面对以上挑战,我们采用如下技术栈进行搭建:
| 技术点 | 技术选型 |
|---|---|
| 框架 | Spring Boot 2.7 + Spring Security 5.7 |
| 数据库 | MySQL + MyBatis Plus |
| Token管理 | JWT |
| 加密方式 | BCryptPasswordEncoder |
| 日志审计 | Slf4j + AOP |
| 安全增强 | Rate Limiter + IP封禁策略 |
架构简图示意:
[客户端]
|
[网关层/Nginx] --> [登录/注册接口]
| ↓
[Spring Security过滤器链]
|
[认证成功 → 返回Token / 认证失败 → 401]
整体架构偏向传统单体应用的前后端分离模式,但为了后期支持微服务架构,我们在Token生成和解析上做了预研,提前预留了刷新机制和跨域鉴权支持。
代码实践:关键配置和流程实现

先来看主流程:登录 → 验证身份 → 返回Token → 后续请求携带Token自动鉴权
1. 添加依赖(pom.xml)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
2. 配置Security主类
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests()
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.build();
}
@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3. 自定义JWT验证过滤器
@Component
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(token);
}
}
chain.doFilter(request, response);
}
}
4. 用户表结构设计(简化版)
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL UNIQUE,
`password` varchar(100) NOT NULL,
`enabled` tinyint(1) DEFAULT '1',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
密码字段长度一定要够大(BCrypt加密后字符串较长),否则会抛出“Data too long”的异常!
踩坑经验:开发过程中的几个典型问题
❗️1. 忽略CSRF设置导致登录报错
刚开始的时候没有关闭CSRF,默认Spring Security会对所有POST请求做校验,结果第一次测试时登录接口就返回403 Forbidden。排查半天才想起要加.csrf().disable()。
建议:如果是前后端分离的应用,推荐关闭CSRF,改用Token来做防CSRF攻击。
❗️2. Filter顺序错误,导致Token没生效
Spring Security的过滤链是有顺序的,如果不小心把JWT过滤器放在其它位置,会导致有些请求压根没走到鉴权逻辑。
正确做法:.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) 这个位置才能覆盖默认的登录处理逻辑。
❗️3. JWT过期时间被系统时间干扰
一开始我们在Token里写死的是绝对时间戳,比如exp: System.currentTimeMillis() + 60_000,后来发现不同服务器之间时间可能存在误差,容易出现Token刚发出就被判定过期的情况。
解决方法:使用LocalDateTime.now().plusMinutes(30).toEpochSecond(ZoneOffset.UTC)这种更稳定的UTC时间处理方式。
❗️4. 密码加密字段长度不足
这是个老生常谈的问题:如果数据库字段不够长,插入的时候就会报错:“Data truncation: Data too long for column 'password' at row...”
BCrypt加密后的密文长度约为60位,MySQL字段至少要设成varchar(100),保险起见可以设到200。
效果总结:项目重构后的收益体现
从最初两周的快速搭建到后面持续迭代,这套基于Spring Security的安全架构为我们带来以下几个显著好处:
- 稳定性强:Spring Security社区成熟,避免了重复造轮子,几乎没有出现重大BUG;
- 易于扩展:如后续增加角色权限控制时,只需自定义
GrantedAuthority即可; - 性能良好:通过Filter链控制访问,响应速度始终维持在合理范围内;
- 兼容性好:和Swagger、MyBatis Plus等主流组件配合顺畅,无明显冲突;
- 运维友好:完善的日志输出和异常处理机制,极大降低了线上排查成本。
最重要的一点是,重构后我们真正实现了“权限即服务”,为后续多租户体系打下了坚实基础。
经验分享:给读者几点真诚建议
如果你正在或打算尝试使用Spring Security来构建自己的认证系统,以下几点是我亲身体验之后想特别强调的:
✅ 不要怕复杂,要学会拆解流程
初学Spring Security很容易陷入“为什么那么多类?为什么流程这么绕?”的困惑。其实只要记住几个核心流程:
- 用户提交账号密码 → 被封装成Authentication → 交给AuthenticationManager → 由Provider去验证 → 成功则放入SecurityContext
理解清楚这几个环节,剩下的就是按需定制细节,比如加Filter、替换PasswordEncoder等。
✅ 注册入口务必单独处理,不要交给Spring Security
很多教程会让你直接写登录接口调用AuthenticationManager.authenticate(...),但对于实际项目来说,登录之外往往还有注册、邮箱验证、短信验证码等步骤。这些最好单独设计接口,只在认证阶段才接入Security的流程。
✅ 日志和监控一定要跟上
特别是生产环境,建议记录每一次登录行为(成功与否都要记录),方便审计和排查。可以通过AOP监听AbstractAuthenticationEvent相关事件实现。
✅ 提前预留权限升级空间
目前我们的认证系统已跑通,下一步就是在现有基础上加上RBAC模型。Spring Security本身支持hasRole()、hasAuthority()等方法,只要设计好权限表关联关系,后续扩展会很轻松。
写在最后:技术路上的成长点滴
说实话,当初接到这个任务时心里也有点发虚。毕竟Spring Security不是个小东西,尤其是它的“约定大于配置”特性让很多新手望而却步。但当你真正沉下心来,跟着官方文档走一遍Demo,再结合实际项目调试修改,你会发现它远没有想象中那么神秘。
这次重构经历让我深刻认识到,一个好的安全系统不仅是代码写得好不好,更重要的是设计是否合理、扩展是否灵活、是否能适应不断变化的需求。Spring Security给了我们一个非常坚实的起点,剩下的就是根据业务实际情况去打磨和完善。
希望这篇来自实战一线的分享,能够帮到那些正在尝试或者刚刚入门Spring Security的朋友。也欢迎大家留言交流,我们可以一起探讨更多关于安全架构的话题。
共勉!

评论 0