从零开始构建安全认证系统:我在Spring Security实践中的那些事儿
开篇 | 起点,往往比想象中更曲折

大概在一年前,我所在的团队负责开发一个企业内部管理系统,用户权限相对复杂,安全要求也比较高。虽然我们过去做过不少后台系统,但这次的需求明确提到了要“具备完整的身份验证与授权体系,并支持多角色分级管理”。于是,Spring Security这个曾经只停留在知识储备层面的技术框架,成了我们的首选。
刚开始信心满满,认为不过就是搭个框架、配几个过滤器的事儿。可真正深入后,才意识到事情并没有那么简单——配置项五花八门,文档版本混乱,自定义逻辑难搞,还有各种各样的“陷阱”等着你去踩。这篇文章就想结合那次项目的实战经历,聊聊我是怎么一步步用 Spring Security 快速搭建起一个基础而实用的认证系统,以及中间踩过的坑和总结的经验。
问题描述 | 不是所有需求都能“默认解决”

项目初期的需求其实不算太复杂:
- 实现基于用户名/密码的登录认证
- 支持不同的用户角色(管理员、运营、普通员工)
- 不同角色访问接口需要不同权限控制
- 登录后的Token机制需长期可用且便于扩展
当时我们的技术栈是 Spring Boot + MyBatis,数据库使用 MySQL,前端是 Vue 框架,整体前后端分离设计。这本来看似很标准的组合,但在实际整合过程中却遇到了一系列挑战:
- 默认的
formLogin配置方式不符合前后端分离的交互模式,需要手动处理 JSON 提交和响应。 - 自定义 UserDetailsService 实现时遇到数据库查询性能瓶颈。
- 多层级权限配置容易出错,尤其在 URL 权限匹配规则上经常“放水”。
- Token 管理没有现成方案,得自己整合 JWT 或 Redis 做状态维护。
这些问题看似细碎,但堆叠在一起就会严重影响交付节奏。尤其是面对上线时间压缩的背景,如何快速高效地搭建出安全认证的核心功能,成了关键。
解决思路 | 以实用为导向的设计哲学

我决定采用渐进式构建 + 核心抽象封装的方式推进:
第一步:明确安全边界与职责
我们先梳理了一个“安全控制矩阵”,明确了哪些接口需要认证,哪些接口需要特定角色才能访问,从而为后续配置URL权限提供了清晰依据。
| 接口路径 | 认证要求 | 角色限制 |
|---|---|---|
| /api/user/list | 是 | 管理员 |
| /api/order/detail | 是 | 运营/管理员 |
| /api/public/info | 否 | 公开 |
第二步:基于 Spring Security 构建核心骨架
核心目标是实现以下功能:
- 用户登录认证(支持账号密码)
- 多角色鉴权
- 使用 JWT 实现无状态会话管理(适合前后端分离)
- 安全上下文持久化与共享
为了提高后期灵活性,我还做了一些初步的抽象封装,比如将用户信息加载服务从 UserDetailsService 抽出来统一管理,避免业务与安全耦合过重。
代码实战 | 关键配置与核心实现
1. 引入依赖
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT工具类 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
2. WebSecurityConfig 类示例
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
private final UserService userDetailsService;
private final JwtRequestFilter jwtRequestFilter;
public WebSecurityConfig(UserService userDetailsService, JwtRequestFilter jwtRequestFilter) {
this.userDetailsService = userDetailsService;
this.jwtRequestFilter = jwtRequestFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/user/list").hasAuthority("ROLE_ADMIN")
.anyEndpoint().authenticated();
return http.build();
}
}
3. JWT 过滤器实现片段
public class JwtRequestFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String tokenHeader = request.getHeader("Authorization");
String username = null;
String token = null;
if (tokenHeader != null && tokenHeader.startsWith("Bearer ")) {
token = tokenHeader.substring(7);
try {
username = jwtUtil.getUsernameFromToken(token);
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT");
return;
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
4. 数据库模型设计参考
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN DEFAULT TRUE
);
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
FOREIGN KEY(user_id) REFERENCES sys_user(id),
FOREIGN KEY(role_id) REFERENCES sys_role(id)
);
踩坑经验 | 那些年我们一起趟过的“雷”
坑一:JWT 过期策略没做好,导致用户频繁掉线
一开始我们直接使用 JWT 的 expireTime 设置了固定时间,结果发现有的用户在线期间 JWT 过期了,又没有自动续签机制,只能重新登录。后来加上了一个简单的刷新机制,在每次请求成功后,如果 JWT 即将到期,就在 header 中返回新的 token。
坑二:忽略跨域设置导致 OPTIONS 请求失败
因为是前后端分离架构,初期忽略了对 CORS 的配置,Spring Security 默认会拦截所有非认证的 OPTIONS 请求。后来我们在 WebSecurityConfig 中加了如下配置:
http.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
同时前端设置好对应的 headers 和 origin 白名单。
坑三:权限表达式写法有误,导致“该拦的没拦住”
我们最初直接用了 .hasRole("ADMIN"),但由于 Spring Security 对 ROLE_ 前缀有内置识别机制,最终修改为 .hasAuthority("ROLE_ADMIN") 才生效。
效果总结 | 变革不止于技术本身
这套安全模块上线后,给整个团队带来了几个明显的变化:
- 安全性提升明显:通过完善的认证流程和角色隔离机制,防止了越权访问等常见漏洞;
- 开发效率提高:后续新接口接入只要按照预设的注解或配置添加权限即可;
- 运维更加可控:日志中可以清晰看到每个请求的认证情况,审计追踪变得容易;
- 扩展性良好:比如后期我们要加入 OAuth 登录时,只需要扩展 AuthenticationProvider 即可完成对接。
最重要的是,这套系统让我们在后续多个项目中都实现了“开箱即用”的权限结构,大大减少了重复开发成本。
经验分享 | 给刚入门同学的一些忠告
不要迷信“全自动配置”
Spring Security 功能强大,但很多默认行为并不适用于真实场景,动手改配置是常态。建议从头开始构建流程图,再逐步添加模块。尽早考虑 Token 状态管理
如果只是简单系统可以用无状态 JWT;如果有高并发、单点登录、强制下线等需求,建议引入 Redis + JWT 结合方案,方便集中管理。权限粒度要分层设计
URL级控制是起点,方法级控制(如 AOP)是进阶,最后才是数据级的动态过滤。别忽视异常处理机制
认证失败、未授权访问这些错误一定要统一格式返回,否则前端很难做全局处理。测试先行
安全问题不容易在运行态发现问题,建议搭配单元测试,模拟各种角色访问,及时排查疏漏。
写在最后 | 安全是底线,也是底气
现在回过头看,那段时间的确挺难,但收获也非常大。从最开始看着 Spring Security 官方文档发懵,到能熟练地调整过滤器链、编写自定义鉴权逻辑、甚至和运维一起优化安全日志记录……每一个细节的背后,都是不断试错、思考和重构的结果。
对于开发者来说,安全不是一项附加功能,而是系统的根基之一。而 Spring Security 就像是一把双刃剑,用得好可以事半功倍,用不好也可能埋下隐患。希望这篇分享能让你少走一点弯路,早点搭出一套属于自己的安全防线。
如果你正在学习或准备落地 Spring Security,不妨试试从最小可行性认证入手,边做边学,一定会更快上手。毕竟,没有什么比亲手跑起来更有说服力了 😊
作者简介:某一线互联网公司后端架构师,多年分布式系统及微服务开发经验,主导过多套平台级系统建设,对 Java 生态、系统安全性设计有持续探索和落地实践。欢迎交流探讨,共同成长。

评论 0