从零开始搭建认证系统:我在Spring Security上踩过的那些坑
引言

还记得去年年底,我们公司要启动一个全新的业务中台项目。作为后端技术负责人,我第一个要考虑的问题就是用户权限和安全控制。
说实话,在这之前我也用过几次Spring Security,但那都是照着教程配个简单的登录页、加几个注解完事。这次不一样——这次我们要做的是一套完整的权限认证系统,集成JWT、支持多租户、还要对接第三方登录(比如钉钉)。任务来得又急,团队里的兄弟也有几个是新来的,对Security这套东西不熟。
一开始我以为这事儿很简单:“Spring Security嘛,配置一下SecurityConfig,再写个UserDetailsService不就好了?”但真正动手之后才发现,事情没那么简单。
今天这篇文,我想以自己的实际经历为线索,带大家走一遍使用Spring Security搭建基础认证系统的过程。不只是贴代码,更想聊聊在真实项目中遇到的问题,以及我怎么一步步解决它们的。
背景与挑战:我们需要一个灵活可扩展的安全框架

新项目是一个面向企业客户的产品管理平台,核心功能是帮助他们集中管理多个App、网站账户。由于涉及到客户敏感数据,安全性必须摆在第一位。
我们的核心需求有:
- 基于账号密码的标准登录流程
- JWT Token机制支持无状态认证
- 支持RBAC权限模型
- 用户角色/权限可动态配置
- 支持对接第三方系统统一鉴权(如SSO)
- 将来可能接入SaaS架构,需预留多租户设计空间
我们最终决定采用 Spring Boot + Spring Security + JWT 的组合,因为:
- 公司技术栈以Java为主,已有一定Spring生态的积累
- Spring Security提供了开箱即用的安全控制能力,社区活跃且文档丰富
- 可通过自定义Filter、AccessDecisionManager等组件实现灵活扩展
但真正的挑战才刚刚开始……
解决方案:Spring Security快速入门实践


Step 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>
然后简单写了两三个核心类:
JwtUtils:处理token生成与解析JwtAuthenticationFilter:拦截请求并校验token合法性SecurityConfig:主配置类
Step 2:核心配置 —— SecurityConfig
这是我写的第一版Security配置类,其实已经基本够用了:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/auth/**").permitAll();
auth.anyRequest().authenticated();
});
return http.build();
}
}
这里的关键点:
- 关闭CSRF保护,因为我们使用的是Token认证而非Session Cookie
- 设置为无状态会话
- 自定义JWT过滤器放在UsernamePasswordAuthenticationFilter前面,先做身份识别
- 接口路径按需放行
这个结构虽然简单,但足够支撑起一个初步可用的安全体系。
实战代码:从头开始实现关键模块

接下来,我会带你看看几个最常用的代码模块是怎么构建的。
1. JWT工具类
@Component
public class JwtUtils {
private static final String SECRET_KEY = "your-secret-key-here"; // 应该从配置文件读取
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 864_000_00))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getExpiration();
}
}
💡注意:
- 不要硬编码密钥(建议使用配置中心或环境变量注入)
- Token有效期可根据实际需要调整
- 在生产环境中,应该加入黑名单、签发时间戳验证等功能
2. 自定义过滤器类
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && jwtUtils.validateToken(token, userDetails)) {
Authentication auth = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
这段代码做了几件事:
- 从Header提取Token
- 解析并验证
- 构建认证对象放入Security上下文中
⚠️ 这里有个常见错误:有些人忘了设置SecurityContextHolder.getContext().setAuthentication(auth),这样后面进行权限判断时就会失败。
遇到的坑与解决方案总结
这一路踩了不少坑,这里我挑几个印象深刻的分享下。
1. 登录接口无法访问?原来是顺序错了!
第一次写完SecurityConfig后,测试登录接口发现居然被拦截了!明明在authorizeHttpRequests中设置了.requestMatchers("/auth/**").permitAll();
最后查了半天发现,是因为我忘记把登录接口放进放行列表之前的位置。如果你自己写了一个登录Filter,记得放到正确的位置。
正确的做法是:
要么在filterChain里主动跳过某些路径;
要么在authorizeHttpRequests中明确允许这些路径访问。
另外一点小技巧:如果不确定哪些Filter被加载了,可以在启动时打印所有注册的Filter。
2. 权限注解失效,原来是缺少@EnableGlobalMethodSecurity
为了后续方便做方法级别的权限控制,我尝试加@PreAuthorize("hasRole('ADMIN')"),结果完全不起作用。
后来查资料才知道,默认情况下Spring Security不会处理这种注解,你必须开启相关支持:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig { ... }
加上这句后,你的方法级权限控制就正常了。
3. 多租户场景下的UserDetailsService冲突问题
我们设计了多租户架构,每个租户都有独立的数据库实例。这时候传统的基于单一源的UserDetailsService就不够用了。
我的做法是:
- 拦截请求时先解析租户标识(比如从Domain头或者子域名)
- 动态选择对应的数据源
- 使用不同的UserDetailsService去查询用户信息
为此我写了个抽象类:
public interface TenantAwareUserDetailsService {
UserDetails loadUserByUsernameAndTenant(String username, String tenantId) throws UsernameNotFoundException;
}
并通过自定义的TenantContext保存当前租户信息,实现逻辑隔离。
效果评估:我们的认证系统现在是什么样?
现在这套系统已经在线上跑了几个月,表现还算稳定。
主要成果如下:
- 成功支撑了初期用户量几千级的QPS压力(结合Redis缓存Token黑白名单)
- 提供了标准的登录、登出、token刷新机制
- RBAC权限模型满足90%以上的业务权限管控需求
- 系统易于扩展,新增租户只需配置数据源即可接入
我们也在逐步完善一些增强功能,比如:
- 结合Spring OAuth2支持更多认证方式(社交登录、手机验证码等)
- 加入审计日志记录登录行为
- 完善权限管理后台界面,允许管理员可视化配置角色权限
我的经验建议:别让Security把你困住

回顾整个过程,几点经验送给大家:
✅ 明确需求,先做好规划
安全不是越重越好,尤其在初创阶段。你得搞清楚:
- 是要做无状态还是有状态?
- 是否需要细粒度的权限控制?
- 后续会不会有多种认证方式? 提前画好架构图、列好需求优先级很重要。
✅ 看懂配置背后干了啥
很多人只会copy粘贴Security配置项,却不知道它到底代表什么含义。建议花点时间阅读Spring Security官方文档的Architecture一节,了解Filter Chain的工作原理。
✅ 不必重复造轮子,但也别盲目依赖框架
像JWT、OAuth2这些模块网上有很多封装库,你可以直接拿来用。但在生产环境下一定要理解其内部机制,比如:
- Token的有效期策略
- 刷新Token的安全性如何保障
- 如果出现大量伪造Token攻击怎么办?
✅ 把认证系统当成基础设施的一部分来维护
认证模块是整个系统的“门卫”。上线后要持续关注以下方面:
- 日志监控登录异常
- 配置告警阈值防止暴力破解
- 定期清理无效token
- 安全加固(IP白名单、WAF)
写在最后:技术成长是个螺旋式上升的过程
回过头来看,当初的我觉得Security好像有点难,甚至一度怀疑是不是应该选Apache Shiro。但现在回头看,正是那次项目让我真真切切理解了Spring Security这套体系。
有时候我们会觉得某个框架太繁琐、不好用,但当你深入了解它的设计思想以后,你会发现它是如此优雅、可扩展。
希望这篇文章能帮你少踩两个坑,少翻两篇文档。毕竟我们开发者的每一分精力,都值得用在更有价值的地方。如果你有任何疑问或者不同看法,欢迎留言交流,我们一起成长 🤝
作者简介:我在一线互联网公司做过多个高并发平台的安全架构设计,目前专注于Spring生态的技术深耕。如果你也喜欢写代码、聊架构、研究技术趋势,欢迎关注我的公众号/博客,一起进步!

评论 0