35岁还在写代码的老程序员:用 Spring Security 搞定认证,顺便准备跳槽
上周五晚上十一点,我一边啃着冷掉的黄焖鸡,一边盯着 IDE 里那个 403 Forbidden 的报错。项目 deadline 是下周一,而安全模块还没跑通。产品经理在群里又发了个“亲,这个登录功能能今天搞定吗?明天演示要用”,我差点把键盘砸了。
但说真的,这年头连个像样的认证都搞不定,还怎么跳槽?最近投了几家大厂,面试官一上来就问:“你们系统是怎么做权限控制的?” 我要是答“靠 Nginx 配置白名单”怕是要被笑死。于是痛定思痛,花了半个月时间,把 Spring Security 从入门到实战撸了一遍——当然,中间少不了 ChatGPT 和 Claude 帮我查文档、改配置(别装了,你也在用,对吧?)。
这篇文章不是照搬官方文档,而是我这个快奔四的老码农,在真实项目里踩坑、调优、上线后总结出来的经验。如果你也正在边刷 LeetCode 边看新机会,那这篇或许能帮你省点时间。
为什么是 Spring Security?Go 语言选手别急着划走
先说清楚,我主栈是 Java,Spring Boot 是日常。虽然最近 Go 火得不行,GitHub 上一堆 auth-service 项目都是用 Gin + JWT 写的,轻量、并发高、部署简单,但我司历史包袱太重——十年前的单体架构,数据库耦合严重,微服务拆不动,只能在现有基础上加固安全层。
Spring Security 虽然“重”,但胜在生态成熟、扩展性强,而且和 Spring Boot 天然集成。更重要的是,面试加分项。你跟面试官说“我用 Spring Security 实现了 RBAC + OAuth2 + 动态权限刷新”,比说“我写了个 middleware 拦截请求”听起来专业多了(哪怕背后逻辑差不多)。
当然,我不是贬低 Go。事实上,我业余也在学 Go,GitHub 上 star 了不少 auth 相关的开源项目,比如 casbin 这种通用权限引擎。但现实是:技术选型不只看性能,还得看团队、看存量、看交付压力。我们团队六个后端,四个只会 Java,领导也不可能让我一个人用 Go 重写认证服务。
所以,务实点,先把 Spring Security 玩明白。
快速搭建:别再复制粘贴那些过时的教程了
网上很多 Spring Security 教程还在教你怎么继承 WebSecurityConfigurerAdapter ——醒醒!Spring Boot 2.7+ 已经废弃它了!现在推荐用 组件注册 + Lambda DSL 的方式配置。
我一开始也是照着三年前的博客配,结果启动报错:
Consider defining a bean of type 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter'...
当时真想骂娘。后来翻了 Spring 官方 GitHub 的 issue 区才发现,新版本要用 SecurityFilterChain Bean。
下面是我现在项目里用的精简版配置(已脱敏),支持表单登录 + JWT 接口认证双模式:
@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/public/**").permitAll()
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtTokenFilter jwtTokenFilter() {
return new JwtTokenFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
几个关键点:
- 关闭 CSRF:因为前后端分离,用 JWT 无状态认证,CSRF 不适用。
- Session 策略设为 STATELESS:避免服务器存 session,减轻内存压力。
- 自定义 JWT Filter 插在 UsernamePasswordAuthenticationFilter 前面:这样
/api/**的请求走 JWT,/login走表单认证。
💡 小技巧:用
requestMatchers().permitAll()而不是antMatchers(),后者也废弃了!
数据库设计:别让权限模型成为性能瓶颈
很多人以为认证就是“查用户密码对不对”,其实真正的挑战在权限粒度和查询效率。
我们最初的设计很 naive:
user表role表user_role关联表permission表(存 URL 路径)role_permission关联表
每次请求进来,都要连查三张表,判断当前用户是否有访问该接口的权限。压测时 QPS 一高,数据库 CPU 直接飙到 90%。
后来优化成两步:
- 登录时一次性加载用户所有权限,缓存在 Redis,Key 是
user:permissions:{userId},Value 是权限字符串集合(如["user:read", "order:create"])。 - JWT Token 中只存 userId,不存权限(避免 Token 过大且权限变更后无法及时失效)。
- 自定义
AccessDecisionManager,在鉴权时从 Redis 读权限,做匹配。
这样,99% 的请求不再碰数据库。Redis 缓存 TTL 设为 30 分钟,配合后台管理界面的“强制刷新用户权限”按钮,基本满足业务需求。
| 方案 | 数据库查询次数/请求 | Redis 查询 | 支持动态权限 | 适合场景 |
|---|---|---|---|---|
| 原始方案 | 3+ | 0 | 是 | 用户量小、权限变更频繁 |
| 缓存方案 | 0(正常情况) | 1 | 是(需主动刷新) | 中大型系统、高并发 |
另外,别把权限路径硬编码在数据库里!比如存 /api/v1/orders/{id} 这种。应该抽象成资源标识符,如 order:read,然后通过 @PreAuthorize("hasPermission('order:read')") 控制。这样即使 API 路径变了,权限逻辑也不用动。
性能陷阱:那些看似无害的配置
Spring Security 默认开启了很多“安全增强”功能,但在高并发场景下可能成为性能杀手。
1. 密码加密别用默认强度
BCryptPasswordEncoder 默认 strength 是 10,加密一次要 100ms 左右。用户登录还好,但如果批量导入用户(比如从旧系统迁移),每秒只能处理 10 个,简直灾难。
解决方案:生产环境用 10,测试/迁移脚本用 4。
// 迁移脚本专用
@Bean
@Profile("migration")
public PasswordEncoder weakPasswordEncoder() {
return new BCryptPasswordEncoder(4);
}
2. 认证失败别记录太多日志
默认配置下,认证失败会打印详细日志,包括用户名(明文!)。攻击者可以利用这点探测有效账号。
我们曾经在线上被扫出几千个“invalid username”日志,运维差点报警。后来重写了 AuthenticationFailureHandler,只记录 IP 和失败次数,用户名脱敏。
3. CORS 配置要小心
很多人为了省事,直接 .cors().and(),结果前端跨域请求被拦。正确的做法是显式配置:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("https://your-frontend.com"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
记住:安全性和便利性永远在博弈。开放 * 虽然爽,但离被挂暗网不远了。
跳槽视角:面试官真正关心什么?
最近面了两家,都被问到 Spring Security 的细节。他们不关心你会不会配,而是关心:
- 如何实现权限动态刷新?
- JWT 如何防重放攻击?
- 如果 Redis 挂了,你的系统会不会裸奔?
我的回答思路:
- 动态刷新:除了 Redis 缓存,我在用户权限变更时,向 Kafka 发一条事件,所有服务实例消费后清除本地缓存(Caffeine)+ Redis 缓存。双重保障。
- JWT 防重放:Token 中加入
jti(JWT ID),并设置短有效期(15分钟)。同时用 Redis 记录已使用的jti,TTL 略大于 Token 有效期。虽然增加一次 Redis 调用,但安全值得。 - 降级策略:Redis 不可用时,自动 fallback 到数据库实时查询(带熔断机制,比如 Hystrix 或 Resilience4j),并告警通知。宁可慢一点,也不能没权限控制。
这些设计不一定完美,但至少说明你考虑过生产环境的复杂性,而不是只会跑通 demo。
最后一点真心话
35 岁还在写代码,说实话有点焦虑。但焦虑没用,不如多写点能放进简历的硬核东西。Spring Security 看似基础,但深挖下去,涉及密码学、缓存、分布式、安全攻防,全是加分项。
我也看过 GitHub 上那些 Go 写的 auth 服务,简洁漂亮。但现实是,大多数公司还在用 Java 维护老系统。能把 Spring Security 用好、调优、讲清楚,比盲目追新更重要。
对了,我把这套认证模块抽成了 starter,放到公司内部 GitLab 了(没敢开源,怕被喷)。如果你也在折腾类似的东西,欢迎交流——说不定下次跳槽,我们能在新公司当同事。
毕竟,谁不想找个不用半夜修 403 的班呢?

评论 0