从零开始,用 Spring Security 快速搭建安全认证系统
引言:一次小需求引发的“大工程”

我之前在一家做 SaaS 的公司负责后端开发,有一次产品提了一个需求:给用户后台加上登录功能,并且根据角色控制不同页面的访问权限。听起来挺简单吧?但当时我们的项目已经上线了一段时间,很多模块都已经成型了,直接加一个登录模块不仅要考虑兼容现有接口,还得保证不破坏原有的功能。
一开始我打算自己写个简单的登录逻辑,结果越想越复杂:密码怎么加密?Token 怎么管理?有没有现成的框架可以快速实现?这时候我想起了以前学过的 Spring Security,于是决定试试看能不能用它来快速搭起一套完整的安全认证系统。
这篇文章就是基于这次实战经验写的,希望能帮你少走点弯路。
问题描述:我们需要什么?

我们当时的系统是一个前后端分离的 REST API 系统,前端使用 Vue,后端是 Spring Boot + MyBatis,数据库用的是 MySQL。
主要诉求如下:
- 用户登录后能获取 Token(比如 JWT)。
- 每次请求都需要带上这个 Token 进行身份验证。
- 不同角色的用户可以访问不同的接口(权限控制)。
- 登录失败要限制尝试次数,防止暴力破解。
- 已有的接口不能因为引入安全机制被破坏掉。
这些问题看起来好像都能靠写一堆拦截器搞定,但实际上一动手就发现要考虑的东西特别多,特别是权限这块,还要和数据库配合设计角色权限表。
解决方案:为什么选择 Spring Security?
Spring Security 虽然学习曲线略陡,但一旦掌握,就能覆盖绝大多数企业级系统的安全需求。我当时之所以选择它,主要是因为它有以下优势:
- 支持多种认证方式:表单登录、JWT、OAuth2、LDAP 等等。
- 权限控制非常灵活,支持方法级别的注解(如
@PreAuthorize)。 - 有丰富的扩展点,可以定制各种行为,比如登录失败处理、自定义过滤器等。
- 社区活跃,文档完善,遇到问题基本上都能找到答案。
更重要的是,它可以很好地整合进已有的 Spring Boot 项目中,不需要你把整个项目重构一遍。
实践过程:一步步搭建认证体系
1. 添加依赖
先引入 Spring Security 和 JWT 相关的依赖(我们最后决定采用 JWT 做无状态认证):
<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. 数据库设计:用户与角色管理
我们原本的用户表只有基础信息字段,现在需要添加角色、权限相关的内容,最终设计如下:
users 表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar | 用户名 |
| password | varchar | 密码(BCrypt 加密) |
| enabled | tinyint | 是否启用账户 |
roles 表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar | 角色名称(如 ROLE_ADMIN) |

user_roles 表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| user_id | bigint | 用户ID |
| role_id | bigint | 角色ID |
有了这些表结构之后,就可以通过 Spring Data JPA 或 MyBatis 查询出用户的角色用于权限判断。
3. 核心配置:SecurityConfig
这是我最花时间调试的地方,尤其是对已有接口的影响控制。下面是简化后的核心配置类:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/auth/login").permitAll()
.anyRequest().authenticated();

return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这段代码做了几件事:
- 关闭 CSRF 防护(因为我们用 Token 认证)
- 设置会话为无状态(Stateless)
- 添加一个 JWT 的前置过滤器
- 开启
@PreAuthorize注解支持 - 对
/login接口放行,其他接口都需认证
4. 实现 JWT 登录流程
我们实现了几个关键类:
JwtUtil: 生成和解析 JWT TokenAuthController: 提供登录接口JwtFilter: 拦截请求,校验 Token 合法性UserDetailsServiceImpl: 根据用户名加载用户和角色信息
以 JwtFilter 为例,它的作用是在请求进入 Controller 之前进行 Token 校验,并设置当前登录用户的上下文:
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = getToken(request);
if (token != null && JwtUtils.validateToken(token)) {
String username = JwtUtils.getUsernameFromToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, null, getAuthorities(username));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
踩坑经验:那些让人崩溃的小细节
说真的,Spring Security 官方文档虽然不错,但有些地方还是太抽象,真正开发时踩了不少坑:
坑一:静态资源访问问题
一开始我们把 /static/** 映射的路径也拦住了,结果前端页面访问不了 CSS 和 JS 文件。后来才意识到要在 configure 方法里放行静态资源:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**", "/api/auth/login");
}
坑二:跨域问题(CORS)
前后端分离场景下,跨域是个常见问题。解决办法有两种:
- 在 Spring Security 中单独配置允许的来源(推荐):
http.cors(withDefaults())
同时提供一个 CorsConfigurationSource Bean。
- 或者在网关层统一处理 CORS(更优雅)。
坑三:异常处理没有统一入口
Spring Security 默认会跳转到 /login 页面或者返回 HTML,但我们是 REST API,必须让它返回 JSON 格式的错误信息。可以通过自定义 AccessDeniedHandler 和 AuthenticationEntryPoint 实现:
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, ex) -> {
response.sendError(HttpStatus.FORBIDDEN.value(), "Forbidden");
};
}
效果总结:稳定又高效的安全体系
这套安全体系上线后,我们团队反馈非常好:
- 登录认证响应速度很快,几乎不影响原有接口性能。
- 所有接口都可以通过
@PreAuthorize("hasRole('ADMIN')")控制权限,开发效率大幅提升。 - 用户权限变更实时生效,无需重启服务。
- 安全防护做得更规范了,审计时也没有暴露明显的漏洞。
而且随着后续接入 OAuth2 和双因素认证,这套架构也具备良好的扩展能力。
经验分享:给初学者的一些建议
如果你正准备上手 Spring Security,这里是我的几点建议:
- 不要怕它难,它其实比你想象的好理解 —— 先跑起来一个最简的例子,再逐步增加功能。
- 关注版本差异,官方文档有时更新滞后 —— GitHub 和社区问答网站有时候更有帮助。
- 一定要做测试! 使用 Postman 或 curl 测试每个接口的安全限制是否生效。
- 权限模型设计要提前规划好 —— 千万别临时改数据库表结构,否则后期麻烦。
- 结合日志排查问题更快捷 —— Spring Security 有很多 debug 日志输出开关,善用它们可以节省大量时间。
结语:安全从来不是附加项
写完这篇回顾性的文章,我也深刻体会到,在一个系统中,“安全”其实从一开始就应该是架构的一部分,而不是事后补上的功能。Spring Security 可以帮助我们在早期就把安全体系构建起来,也能让后续的维护变得更轻松。
希望这篇真实经历能对正在学习或准备使用 Spring Security 的你有所帮助。如果你在使用过程中遇到任何问题,欢迎留言交流,我们一起成长 🌱

评论 0