技术文章
聊聊我在杭州写Spring Security的踩坑经历
敲下这行字的时候,我刚在Vim里把最后一个Spark SQL的UDF优化完,顺手按了:wq保存退出。坐标杭州,做大数据开发刚好满三年。天天跟Spark、Flink打交道,Hadoop集群的坑基本踩了个遍。本来日子过得挺滋润,但最近大环境太卷,阿里和网易那边放出来的HC虽然多,但JD里动不动就要求“具备全栈能力”或者“熟悉后端微服务架构”。
为了下半年能跳槽涨点薪,我只能硬着头皮捡起以前学过一点的Spring Boot。刚好上周,我们组内部要搞个数据资产管理的后台,领导说:“小X,你大数据搞得不错,这个内部系统的后端你也顺手接了吧,用Spring Security做个鉴权。”得,这下连摸鱼的借口都没了。
说实话,刚接手时我满脑子都是怎么用Go去重写个高并发网关,毕竟最近自己私下里挺喜欢折腾Go这种轻量级语言,写起来那叫一个丝滑。但工作嘛,还是得求稳,Java生态毕竟摆在那,而且组里其他兄弟接手也方便。本来偷懒想用AI写作工具直接生成一套Security配置,结果生成的代码全是老掉牙的继承WebSecurityConfigurerAdapter,在Spring Boot 3.x里直接标红报错。没办法,只能关掉插件,自己手动Continue,一点点啃官方文档。
既然是实战,咱就不整那些虚头巴脑的理论,直接上干货。
架构与数据库设计
先说数据库设计。很多新手一上来就建个user表完事,这在生产环境绝对要挨骂。考虑到后续可能要跟公司内部的RBAC(基于角色的访问控制)系统打通,我设计了标准的三表结构:sys_user、sys_role、sys_permission,外加两张关联表。
这里有个接口设计的考量:因为我们是内部数据系统,QPS虽然不高,但数据权限要求极严。比如A部门的人绝对不能看到B部门的Spark作业日志和底层Hive表。所以在sys_permission表里,我特意加了data_scope字段,用来做行级别的数据权限隔离。这比单纯在代码里写死if (user.getDeptId().equals(...))要优雅得多,后续扩展也方便。
核心代码实现
接下来是重头戏,Spring Security的配置。千万别去网上抄那些几年前的博客了,现在都Spring Boot 3了,老写法直接废。
先上核心的SecurityConfig:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用csrf,前后端分离项目标配,不然POST请求全报403
.csrf(csrf -> csrf.disable())
// 配置跨域,这里踩了个大坑,后面细说
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 基于JWT,所以不需要session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// 放行登录接口和swagger文档
.requestMatchers("/auth/login", "/v3/api-docs/**").permitAll()
// 其他所有请求都需要认证
.anyRequest().authenticated()
)
// 添加自定义的JWT过滤器,放在UsernamePasswordAuthenticationFilter前面
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 省略CORS配置和JWT Filter的Bean注册
}
看着挺简单对吧?但里面的JwtAuthenticationFilter才是核心。每次请求过来,拦截器从Header里抠出Token,解析出用户信息,然后塞进SecurityContextHolder里。
这里有个性能设计的细节:解析Token和查数据库校验权限是很耗时的。为了扛住偶尔的批量数据导出请求,我在Filter里加了本地缓存(Caffeine),把解析后的用户权限缓存5分钟。别小看这几行代码,压测的时候接口RT(响应时间)直接降了30%,领导看了还夸我优化做得好,其实也就是加了个缓存的事。
踩坑与生产运维经验
代码写完,本地跑起来美滋滋,一上测试环境就炸了。
第一个坑是CORS跨域。前端小哥拿着Swagger调试,直接给我甩了个“接口403,你后端是不是没配跨域”。我一看日志,好家伙,OPTIONS预检请求直接被Security拦截了。后来在CorsConfiguration里把allowedMethods和allowedHeaders配全,并且在Security链里把cors()的优先级提到最高,才算搞定。这里提醒大家,跨域配置一定要在Security过滤器链的最前面生效。
第二个坑更搞心态。上周五晚上快下班了,测试妹子跑过来找我:“这个系统怎么每隔半小时就让我重新登录一次?”我一看,JWT的过期时间我设的是30分钟。无状态Token的通病就是没法主动续期,时间一到直接踢人。
当时真的想砸电脑,但想想明天还要发版,只能硬着头皮改。最后搞了个“双Token”机制:Access Token 30分钟过期,Refresh Token 7天过期。前端在Access Token快过期时,静默调用刷新接口换取新Token。虽然代码量翻倍,但用户体验总算正常了。
说到生产运维,这里分享点血泪经验。上了生产后,有一次运维大哥半夜给我打电话,说网关日志里狂刷Security的认证失败报错。我爬起来一看,原来是有个外网IP在疯狂爆破我们的登录接口。
赶紧加了个限流策略,在Nginx层对/auth/login接口做了IP频次限制,同时在Spring Security里加了个简单的失败次数计数器,连续错5次直接锁定账号15分钟。这里建议大家在生产环境一定要把Security的日志级别调到INFO或者DEBUG(排查问题时),并且把认证失败的日志打到单独的日志文件里,方便后续用ELK做安全审计。
总结
折腾了大半个月,这个内部数据资产管理系统总算平稳上线了。虽然中间被前端吐槽过接口文档不清晰,被测试妹子抱怨过登录太频繁,但看着自己亲手搭起来的安全认证系统稳稳地跑在生产环境,心里还是挺有成就感的。
这次跨界写后端,让我对Spring Security的过滤器链模型有了更深的理解。其实不管是搞大数据还是写后端,底层的架构思想都是相通的。比如Security的过滤器链,跟Spark里的DAG执行计划、Pipeline机制,本质上都是对数据(请求)进行一系列有向的转换和过滤。
不说了,产品经理又在建群@我了,估计又是哪个报表的数据对不上。我得去查查Hive元数据了。希望这篇文章能给同样在折腾Spring Security的兄弟们避避坑,祝大家写的代码都没有Bug,永不宕机!

评论 0