一个被 Spring Security 救命的研二狗:从零搭认证系统的血泪史
大家好,我是杭州某211软工研二在读,目前在实验室一边肝毕业课题一边接校企合作项目。最近我们组接了个政务系统的活儿,甲方爸爸要求“必须上认证授权”,而我这个 Rust 爱好者(是的,最近沉迷 unsafe 和 ownership 无法自拔)居然被派去搞 Java 后端——因为组长说:“你不是天天用 ChatGPT 写代码吗?那肯定啥都能搞。”
行吧,谁让我平时靠 Claude + GitHub Copilot 混日子呢。但这次真翻车了:一开始我试图用 Go 重写整个后端(毕竟 Go 的 gin + jwt 那套我熟),结果被导师一句“甲方明确要求 Spring Boot 技术栈”打回原形。得,那就硬着头皮上 Spring Security 吧。
起因:产品经理一句话,后端肝三天
事情发生在上周三下午,产品会上 PM 轻描淡写地说:“登录注册这块,记得加个 RBAC 权限控制啊,不同角色看不同菜单。”
我当时表面微笑点头,内心已经骂了八百遍——这都快交付了才提权限模型?!
更离谱的是,运维还补刀:“别用 shiro,上次隔壁组用 shiro 漏洞被扫出来,安全团队差点没把他们组开除。”
好家伙,直接给我锁死技术选型:Spring Boot + Spring Security。
别被文档劝退:Security 其实没那么可怕
说实话,第一次看 Spring Security 官方文档的时候,我差点以为自己在读哲学论文。“AuthenticationManagerProviderTokenFilterChain…” 这些词堆在一起,像极了我导师写基金申请书时的状态。
但当我放下偏见,结合 ChatGTP 给的 demo 一跑,发现其实核心就三件事:
- 用户怎么登录?(表单 or JWT)
- 权限怎么校验?(角色 or 权限码)
- 请求怎么放行?(哪些接口不用登录)
我们项目是前后端分离架构,所以果断选 JWT 无状态认证。下面是我踩坑后整理的最小可行方案。
核心配置:别再抄网上过时的教程了!
很多博客还在教 WebSecurityConfigurerAdapter,但 Spring Boot 2.7+ 已经弃用了!现在推荐用 SecurityFilterChain Bean 方式配置。这是我最终稳定的配置类(已脱敏):
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 前后端分离可关
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
注意几个关键点:
csrf().disable():前后端分离项目通常不需要 CSRF 保护(但如果你有同域 cookie 认证另说)SessionCreationPolicy.STATELESS:彻底禁用 session,符合 JWT 无状态特性addFilterBefore:自定义 JWT 拦截器插在用户名密码过滤器前面
自定义 JWT 拦截器:别让 token 成摆设
很多新手以为加个拦截器就完事了,结果线上被绕过权限——因为没做 SecurityContext 的手动注入!
我的 JwtAuthenticationFilter 核心逻辑如下:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && JwtUtil.validateToken(token)) {
String username = JwtUtil.getUsernameFromToken(token);
// 手动构造 Authentication 对象并塞入 SecurityContext
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
血泪教训:如果不调用 SecurityContextHolder.getContext().setAuthentication(auth),后续的 @PreAuthorize("hasRole('ADMIN')") 会直接失效!因为 Spring Security 根本不知道当前用户是谁。
数据库设计:别把角色和权限混为一谈
我们最初偷懒,直接让用户表存一个 role 字段(值为 "admin"/"user")。结果 PM 第二天就说要支持“数据权限”和“细粒度按钮权限”。
于是紧急重构为经典 RBAC 模型:
| 表名 | 说明 |
|---|---|
sys_user |
用户表(id, username, password) |
sys_role |
角色表(id, name, code) |
sys_permission |
权限表(id, name, code, url, method) |
user_role |
用户-角色关联表 |
role_permission |
角色-权限关联表 |
这样,权限校验可以做到接口级别:
@GetMapping("/api/orders")
@PreAuthorize("hasPermission('order:read')")
public List<Order> getOrders() { ... }
配合自定义 PermissionEvaluator,还能实现动态权限判断(比如“只能看自己创建的订单”)。
性能与安全:生产环境避坑指南
上线前我们做了压力测试,发现两个大坑:
每次请求都查 DB?太奢侈了!
解决方案:在UserDetailsServiceImpl中加入 Redis 缓存。用户登录后,把权限列表序列化存入 Redis(key:auth:perms:{userId}),拦截器优先读缓存。JWT 黑名单怎么做?
虽然 JWT 本身无法 revoke,但我们可以在登出时把 token 加入 Redis 黑名单(带 TTL=token 过期时间)。拦截器校验时先查黑名单。
// 登出示例
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpServletRequest request) {
String token = extractToken(request);
if (token != null) {
redisTemplate.opsForValue().set("jwt:blacklist:" + token, "",
JwtUtil.getRemainingTime(token), TimeUnit.MILLISECONDS);
}
return ResponseEntity.ok().build();
}
为什么我不用 Go 重写?现实很骨感
其实我一直想用 Go 重构(Gin + GORM + Casbin 那套真的香),但现实很残酷:
- 团队技术栈统一:实验室其他同学只会 Java,Go 项目没人接手
- Spring 生态太强:Actuator 监控、Sleuth 链路追踪、Spring Cloud Config,这些在 Go 里要拼凑多个库
- 甲方审计要求:政府项目明确要求使用“主流企业级框架”
不过私下我拿 Rust 写了个 JWT 解析小工具(用 jsonwebtoken crate),性能比 Java 快 3 倍,可惜只能当玩具 😅。
最后:Security 不是银弹,但能救命
上周五晚上 9 点,安全扫描报告终于过了——0 高危漏洞。那一刻我瘫在椅子上,感觉比跑通第一个 Rust borrow checker 还爽。
回头想想,Spring Security 虽然配置复杂,但一旦搭好架子,后续扩展非常稳。而且结合 Spring Boot 的自动装配,其实 80% 的场景都有成熟方案。
给学弟学妹的建议:别怕 Security,它只是看起来凶。先跑通最小流程,再逐步加功能。遇到问题多看源码(AbstractAuthenticationProcessingFilter 是个好起点),少信三年前的 CSDN 博客。
对了,如果你们也在杭州搞 Java,欢迎交流~阿里网易这边 Security 岗需求不少,据说懂 OAuth2 + SSO 的简历直接进面试。我?还在改论文,顺便刷 LeetCode 准备秋招……(叹气)
注:本文所有代码均经过生产验证,但为保护项目隐私做了简化。如需完整 DEMO,可私信 GitHub 仓库(带详细 README)

评论 0