从零到一:用 Spring Security 快速搭建安全认证系统
开篇背景:一个“不设防”的业务系统

去年我加入了一个新项目,是一个典型的 ToB 系统,用户包括内部员工和外部合作伙伴。一开始团队为了快速验证产品逻辑,没有在权限控制上做太多考虑,整个系统几乎是“裸奔”状态,接口可以被任意访问,登录功能也只是个简单的 token 校验。
但随着系统上线进入测试阶段,问题接踵而至:有开发人员误删了生产数据、测试账号之间互相访问彼此的业务记录、甚至还有外部扫描工具发现了未保护的 API 接口。
作为负责后端架构的主要成员之一,我意识到必须尽快搭建一套完整的认证授权体系,而考虑到我们是基于 Spring Boot 构建的服务,Spring Security 成为首选。
这篇文章就是我在这次实战中总结下来的经验,希望能帮助你少踩坑、快上手。
遇到的问题与挑战:权限失控带来的隐患

1. 接口无保护,存在越权访问风险
所有接口都没有校验用户身份,任何知道 URL 的人都可以访问,并且部分业务接口还存在根据 userId 查询数据的操作(比如 /api/user/{id}),很容易出现 A 用户通过修改 id 查看 B 用户数据的情况。
2. 缺乏统一登录机制
虽然已经有前端登录页面,但后端只是在登录时返回一个简单的 token 字符串,没有任何时效性管理或续期机制,也没有将 token 和用户权限关联起来。
3. 角色权限体系缺失
系统里应该区分 admin、operator、guest 这样的角色,但在原有结构下完全没有体现出来。
4. 安全漏洞难以排查
随着第三方审计介入,发现了很多潜在的安全漏洞,比如 CSRF、XSS 漏洞检测失败、默认错误信息暴露版本号等。
这些问题如果不及时处理,一旦系统正式上线,后果不堪设想。
我的选择:为什么选 Spring Security?

当时团队评估过几个方案:
- Shiro:更轻量,适合对安全模块要求不太复杂的场景;
- 自研 Token 认证中间件:可控性强,但开发周期长;
- Spring Security:集成度高,社区活跃,文档完善,适合企业级应用;
最终我们选择了 Spring Security + JWT + OAuth2 的组合。这套体系不仅能解决基础的认证授权需求,也为未来接入 SSO、OAuth 第三方登录预留了空间。
而且 Spring Security 提供了完整的 FilterChain 流程控制,我们可以很方便地插入手动编写的鉴权逻辑。
实施过程:一步步构建起防线
我们的系统是典型的微服务架构,不过这次我们先集中在网关层和认证中心的建设上,以下是关键步骤:
1. 初步启用 Spring Security,默认拦截所有请求
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.anyRequest().authenticated();

return http.build();
}
}
这一步其实只是为了测试环境是否已经接入成功。这时候所有接口都会自动拒绝匿名访问,并抛出 401 错误。虽然还不具备真正的登录能力,但至少把门关上了。
2. 自定义 JWT 登录流程
我们使用 JWT 来做无状态认证,所以第一步要实现登录接口:
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginDto dto) {
String token = authService.login(dto.getUsername(), dto.getPassword());
return ResponseEntity.ok(token);
}
}

JWT 工具类生成 token 示例:
String jwt = Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24小时有效期
.signWith(SignatureAlgorithm.HS512, "my-secret-key")
.compact();
3. 添加 JWT 认证过滤器链
创建一个 JWT 认证过滤器,插入到 Spring Security 的 FilterChain 中:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String token = getTokenFromRequest(request);
if (token != null && jwtUtil.validateToken(token)) {
UsernamePasswordAuthenticationToken authentication = jwtUtil.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
// ...
}
// 插入到 UsernamePasswordAuthenticationFilter 之前
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
// ...其他配置
}
4. RBAC 动态权限控制设计
我们参考的是经典的 RBAC 模型:
- 用户表(User)
- 角色表(Role)
- 权限表(Permission)
- 用户-角色映射表(UserRole)
- 角色-权限映射表(RolePermission)
然后,在接口层面配合 Spring Security 的方法级注解来控制访问权限:
@GetMapping("/admin-panel")
@PreAuthorize("hasAuthority('PERMISSION_ADMIN_PANEL')")
public String getAdminPanel() {
return "Welcome to Admin Panel";
}
为了让这个机制能动态支持不同的权限,我们在启动时就把数据库中的权限加载进内存并缓存起来:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/**").access("@dynamicSecurityService.checkPermission(authentication,request)")
.anyRequest().authenticated();
}
这段代码里的 @dynamicSecurityService.checkPermission 是通过 Spring EL 表达式调用我们自己写的权限判断服务,实现灵活控制。
5. 跨域与安全头补缺
前后端分离之后,跨域问题必须处理,我们添加了全局配置:
@Configuration
@RequiredArgsConstructor
public class CorsConfig implements WebMvcConfigurer {
private final Environment env;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(env.getProperty("frontend.url"))
.allowCredentials(true)
.allowedMethods("*");
}
}
另外,在响应头中加入了基本的 CSP、X-Content-Type-Options 等内容防止 XSS:
http.headers()
.contentSecurityPolicy("default-src 'self'; script-src 'self' https://trusted-cdn.com")
.and()
.xssProtection().block(true);
效果与收益:系统终于有了安全感
这套机制上线后,我们收获了很多好处:
✅ 安全性大大提升
- 所有接口都经过身份校验
- 不同用户只能访问自己的数据,RBAC 控制权限细粒度化
- 日志中可以追踪每个请求是由哪个用户发起的
✅ 接口文档更规范、更清晰
由于引入了 Spring Security 注解风格的权限标记,Swagger 可以自动识别哪些接口需要什么权限,也方便测试团队理解。
✅ 性能影响几乎不可察觉
因为 JWT 是无状态认证机制,不需要频繁查询数据库。我们在压测环境下模拟了并发登录 + 多接口调用,整体性能损耗不到 5ms。
✅ 后续扩展成本降低
当我们后来接入 OIDC、集成钉钉扫码登录时,Spring Security 已有的结构让我们可以轻松新增各种 Provider,省去了很多重复开发工作。
经验分享:踩过的坑,希望你不用再踩
⚠️ 小心 Session 管理方式的影响
Spring Security 默认使用 session-based 认证,对于前后端分离的应用建议关闭,改为 stateless:
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
不然你会发现每次重启服务,用户都需要重新登录,或者 session 数据丢失。
⚠️ 方法级别的权限注解不是银弹
像 @PreAuthorize 和 @Secured 这种注解非常实用,但也有局限性:
- 需要在配置中显式开启
@EnableGlobalMethodSecurity - 修改权限后不会即时生效,除非重启或清空缓存
- 对于大量接口时维护成本很高,推荐搭配动态路由策略或接口权限中心一起使用
⚠️ 数据库权限模型要尽早设计好
我们是在系统中期才开始梳理权限结构的,结果发现很多接口缺少明确权限边界,临时加权限的时候容易漏掉。
建议大家从初期就规划好各个模块的权限点,并在接口开发时同步打上注释或注解,减少后期重构成本。
⚠️ 生产环境务必开启详细的日志追踪
我们早期在日志中记录了用户 ID + 请求路径 + HTTP Method,后续结合日志分析平台做了很多行为追踪和异常监控,非常有用。
比如,我们可以快速定位是谁删除了哪条数据,或者某个敏感接口被谁高频调用,这些都能帮助运营和安全部门做出决策。
结语:不只是工具,更是一套方法论
回过头来看,Spring Security 其实不仅仅是一个框架,它背后蕴含着一整套认证、授权、会话管理的方法论。我在实际项目中也不断调整思路,比如最开始以为只要加个 JWT 解析就可以,后来才发现还需要 RBAC 支持、CSRF 防御、动态权限加载等。
如今我们这套认证系统已经在多个项目中复用,成为公司统一的用户中心一部分。而我对 Spring Security 的理解和信心,也是在一次次上线、报错、修复过程中建立起来的。
如果你也在构建一个需要权限控制的系统,不妨尝试一下这套方案。也许刚开始会觉得复杂,但坚持用下来,你会发现它远比你想象的强大得多。
📌 最后送你一句心得:
“安全是个持续的过程,永远没有‘完成’这一说。”
不要追求完美,而是要让系统具备可演化、易维护的能力。

评论 0