从零搭建安全认证系统:我在项目中踩过的那些“坑”和学到的经验
背景:一个看似简单的权限需求,让我彻夜难眠

去年年初,我所在的互联网公司要上线一个内部管理系统。作为后端负责人,我接到的需求是:“这个系统需要有用户登录、角色权限管理,还有对外的接口调用权限控制。”听起来似乎是个很常规的开发任务。
但当我真正开始动手设计权限模块时,问题接踵而至:
- 如何统一处理所有请求的权限判断?
- 用户登录如何做 Token 认证?JWT 还是 Session?
- 不同角色能访问不同接口,这个怎么配置才灵活?
- 多租户环境下权限该怎么设计?
- 权限数据变更后,如何不重启服务生效?
我意识到,这不再是一个简单的拦截器+数据库鉴权的方案可以搞定的事情了。我们需要一个更系统、可扩展、且性能可控的安全机制。于是,我决定尝试使用 Spring Security 来构建整个安全认证体系。
这篇文章,我想跟你分享我当时是如何一步步搭建起这个系统的,以及过程中踩过的一些坑和一些实战经验。
第一步:为什么选择 Spring Security?

在 Java 后端生态中,Spring Security 是目前最主流的安全框架。虽然一开始学习曲线略陡,但一旦掌握之后它的灵活性和稳定性远胜于自己手写权限逻辑。
当时我们团队的选型会议中也讨论过其他方案,比如 Apache Shiro。但考虑到我们的系统将来可能会接入 OAuth2、支持多租户、甚至对接企业微信/钉钉,最终我们选择了 Spring Security——它对这些场景的支持更好,社区活跃度也很高。
架构设计:安全层应该放在哪里?

我们在系统架构上做了如下分层设计:
浏览器/移动端 → Nginx(负载) → Gateway(网关鉴权) → Spring Boot 微服务
这里的关键点在于:鉴权不是只在业务服务里做,而是应该前置到 Gateway 层。
这样做的好处很明显:
- 减少每个微服务重复鉴权的负担;
- 可以在网关实现全局权限控制;
- 接口级别的黑白名单可以快速生效;
- 更容易支持 JWT、OAuth 等多种认证方式。
不过,这也带来了一个问题:Spring Security 默认是在 Web 容器中初始化的,而 Gateway 是独立的服务。所以我们必须把 Spring Security 的 FilterChain 和 Authentication 机制迁移到 Gateway 中(后面会细讲)。
登录流程设计:Token 认证 vs Session 认证
我们在调研阶段对两种方案做了对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Session 认证 | 安全性好,服务端可控性强 | 集群部署下需 Session 共享 |
| JWT Token 认证 | 无状态,便于水平扩展 | Token 注销困难、安全性依赖加密算法 |
结合我们系统未来可能的扩展性考虑(比如多集群、跨地域部署),最终我们选择了 基于 JWT 的 Token 认证方案。
整个登录流程大致如下:
- 用户提交用户名密码到
/login接口; - 校验通过后,生成带有
userId和exp的 JWT; - 将 Token 返回给客户端;
- 客户端后续请求带上该 Token;
- 拦截器解析 Token 获取当前用户信息,并完成权限校验;
- 若合法则进入 Controller;否则返回 401。
在这个过程中,我遇到的第一个问题是:如何在 Spring Security 中集成 JWT 认证机制?
技术实现:自定义 Token 过滤器 + 动态权限加载
1. 创建 JWT 工具类
我们使用的是 Java-JWT 库来处理 Token 的签发与验证:
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.claim("authorities", userPrincipal.getAuthorities())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
这里的 Authentication 是 Spring Security 提供的认证对象,后续我们会手动创建并交由 Spring Security 管理。
2. 自定义 JWTFilter 实现 Token 解析
为了将 Token 解析后的结果注入到 Spring Security 的上下文中,我们需要自定义一个过滤器:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && validateToken(token)) {
Authentication auth = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private Authentication getAuthentication(String token) {
// 解析 Token 并构造 UsernamePasswordAuthenticationToken
}
}
这个过滤器的作用就是每次请求都检查是否有 Token,并将其转换为 Spring Security 能识别的 Authentication 对象。
3. 加载用户信息和权限
接下来,我们需要在用户登录成功后构建一个完整的 Authentication 对象。这里涉及到两个关键类:
UserDetailsService:根据用户名获取用户信息;GrantedAuthority:封装用户的权限列表。
我们的实现方式是:
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
getAuthority(user.getRoles())
);
}
private Collection<? extends GrantedAuthority> getAuthority(Collection<RoleEntity> roles) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (RoleEntity role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
}
然后,在登录接口中:
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtUtils.generateToken(authentication);
return ResponseEntity.ok().body(new JwtResponse(token));
}
这样,我们就能让 Spring Security 在后续的权限判断中正常使用这套认证机制了。
动态权限控制:@PreAuthorize 和 URL 匹配的双保险
为了实现接口级别的权限控制,我们主要用了两种方式:
1. 方法级别的注解:@PreAuthorize
这是 Spring Security 的预授权机制,可以在方法执行前进行权限判断:
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/info")
public String adminInfo() {
return "Admin page";
}
这种写法非常直观,适合权限粒度较粗的场景。
但也有缺点:修改权限需要改代码重新发布。因此我们并没有大规模使用,主要用于管理员后台等固定角色接口。
2. URL级别的权限控制:动态加载 + FilterSecurityInterceptor
我们采用的方式是在运行时从数据库加载权限规则,然后动态注入到 Spring Security 的 FilterChain 中。
这部分涉及较为复杂的配置,核心思路是:
- 在启动或权限变更时加载权限表;
- 构建 URL 到权限名称的映射;
- 实现
SecurityMetadataSource接口返回 URL 所需的权限; - 使用
AccessDecisionManager做权限决策。
这种方式的优点是权限可以动态更新,无需重启服务。当然代价是复杂度高了一点。
举个例子,我们有一张权限表结构如下:
| id | url_pattern | required_role |
|---|---|---|
| 1 | /api/user/** | ROLE_USER |
| 2 | /api/admin/** | ROLE_ADMIN |
每当权限发生变化,我们就触发一次 updateSecurityConfig() 方法,刷新 Spring Security 内部的匹配规则。
生产环境中的运维问题和应对策略
1. Token 有效期太短,用户频繁登录怎么办?

初期我们设置的 Token 有效时间是 1 小时,结果测试部门反馈“经常要重新登录”,体验很差。
解决方法是引入 Refresh Token:
- Token 用于短期请求;
- Refresh Token 有效期长,用于换取新的 Token;
- Refresh Token 存储在 Redis 中,失效后需重新登录。
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken(HttpServletRequest request) {
String token = extractToken(request);
String refreshedToken = jwtUtil.refreshToken(token);
return ResponseEntity.ok(refreshedToken);
}

这样既能保证安全,又不至于让用户频繁输入账号密码。
2. 多租户下的权限冲突问题
我们后来在系统中接入了“多租户”的需求。这就带来了新的挑战:同一权限名在不同租户下含义不同。
例如:
TENANT_A 有一个名为 ROLE_ADMIN 的角色;
TENANT_B 也有一个名为 ROLE_ADMIN 的角色。
如果不加区分,Spring Security 会误认为它们是同一个权限!
我们的解决方案是:在权限名称前加上租户标识,如:
ROLE_TENANT_A_ADMIN
同时,在登录后记录当前 TenantId,并在权限校验时一并带入。
3. 权限缓存导致权限更新不及时
有时候,运维人员更新了用户的角色,但前端仍然显示没有权限。
这是因为 Spring Security 默认不会自动刷新用户的权限信息。
我们的做法是:在用户权限更新后,主动清理掉对应用户的 SecurityContext,并让他下次请求时重新拉取最新的权限。
项目收益:不只是功能,更是架构上的提升
这套认证系统上线后,整体效果非常不错:
- 登录流程稳定,未出现大规模 Token 失效问题;
- 接口权限控制灵活,可随时调整;
- 支持多租户、API级别权限,满足了各种扩展需求;
- 减少了大量手工权限校验代码,提升了维护效率。
更重要的是,这套权限模型为后续接入 OAuth2、SSO、企业微信登录等高级功能打下了坚实基础。
给你的建议:Spring Security 最实用的几个Tips
作为一个踩过很多坑的开发者,我想给你分享一些亲身体验:
不要试图绕开 Spring Security 的机制,尤其是初学者,很容易陷入“自己搞一套”的陷阱。其实它的默认行为都是经过长期打磨的,别轻易重写。
优先使用方法注解做权限控制,除非你真的需要动态加载权限。大多数场景已经够用了。
JWT 的 Secret 必须保密,千万不要硬编码在代码里!建议使用 K8s Secret 或者 Vault 来集中管理敏感信息。
权限配置最好可视化,比如做一个后台界面专门管理 URL 对应的角色权限,方便产品同学操作。
监控 Token 的生命周期,包括生成、刷新、失效、吊销。可以用 Prometheus + Grafana 做指标看板。
测试一定要覆盖权限边界情况,比如一个角色同时拥有多个权限,或者某接口没有任何权限的情况。
不要迷信 Token 认证就是万能的,有些场景 Session 依然适用,尤其在内网系统中。
结语:安全不是一时的工程,而是一种持续的保障
回想起当初刚接手这个项目的忐忑,到现在看着系统稳定运行,我觉得最大的收获不仅是掌握了 Spring Security 的使用技巧,更重要的是对“系统安全”这件事有了更深的理解。
它不仅仅是防止别人非法登录,更是对整个系统的一种责任和敬畏。
如果你正在搭建自己的认证系统,不妨试试 Spring Security。也许刚开始会觉得复杂,但一旦理解它的运作机制,你会发现它就像一位老练的守门人,默默守护着你的应用。
愿你在技术之路上越走越远,写出更加健壮、安全、优雅的系统!

评论 0