Spring Security上手就翻车?我在新东家两周踩过的坑全在这了
入职新公司才俩月,上周五晚上十点半,我还在会议室和前端小哥对认证接口。空调坏了,汗流浃背,脑子里却全是 403 Forbidden 的报错——这该死的 Spring Security,怎么又拦了我的 /api/user/profile?
我是老京东人,干了五年后端,经历过三次双11零点洪峰、四次618大促压测,自认对高并发、限流、熔断这些“硬菜”还算拿手。但万万没想到,刚跳槽到这家创业公司(美其名曰“高速成长型科技企业”),第一个任务就是搭个用户认证系统,结果被 Spring Security 教做人。
更尴尬的是,我以前在京东基本靠内部框架,安全模块都是封装好的,这次裸奔上阵,才发现自己连 UsernamePasswordAuthenticationFilter 都没亲手写过。还好有 Claude 和 ChatGPT 救命,不然真得在新人试用期就被劝退了。
为啥突然要搞认证?还得兼容Python?
事情是这样的:公司之前是个纯 Python 后端团队,用 FastAPI + JWT 搞了一套登录逻辑。现在要上新业务,领导拍板:“后端语言统一成 Java,但旧接口不能动!” 于是我们 Java 组就得接锅——既要支持新前端(Vue3)调用新 API,又要让老 Python 服务能平滑对接认证体系。
产品经理还补刀一句:“下周三上线,行吧?” 我表面微笑,心里已经把 Spring Security 的文档翻烂了。
第一个坑:路径匹配规则把我整懵了
一开始我以为加个 @EnableWebSecurity 就完事了。结果一启动,所有接口都被拦了,连 /health 都返回 401。查了半天,发现新版 Spring Security(6.x)默认启用了 CSRF,并且 所有请求路径都受保护,除非你显式放行。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 先关掉CSRF,前端是SPA,用不了这个
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatcher(new AntPathRequestMatcher("/actuator/**")).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
注意这里用的是 requestMatchers() 而不是老版本的 antMatchers(),API 变了!而且如果你用了 WebSecurityCustomizer 去 ignore 路径,它会完全绕过 SecurityFilterChain,导致你的自定义过滤器也失效——这点坑了我整整一天。
第二个坑:JWT 解析失败,因为Header写错了
前端小哥按常规写了 Authorization: Bearer <token>,但我的解析逻辑一直报 Invalid JWT。最后发现,他在 Axios 拦截器里拼错了 Header 名,写成了 Authorizaton(少了个 r)…… 这种低级错误,测试居然没测出来!
但这也提醒我:认证系统的容错性必须强。于是我加了个前置过滤器,专门处理 Header 格式:
public class JwtHeaderNormalizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
// 正常流程
} else if (authHeader != null && authHeader.contains(" ")) {
// 兼容前端可能传错格式的情况
String[] parts = authHeader.split(" ");
if (parts.length == 2) {
request = new HttpServletRequestWrapper(request) {
@Override
public String getHeader(String name) {
if ("Authorization".equalsIgnoreCase(name)) {
return "Bearer " + parts[1];
}
return super.getHeader(name);
}
};
}
}
chain.doFilter(request, response);
}
}
别笑,这种“脏活”在真实项目里太常见了。毕竟前端同学可能同时维护三个项目,手抖是常态。
第三个坑:和Python服务共享Token,密钥管理炸了
旧 Python 系统用的是 HS256 签名,密钥写在 .env 里。我们 Java 服务也得用同一套密钥验签,否则用户登录后调 Python 接口会失败。
问题来了:密钥放哪儿?
- 放
application.yml?会被 Git 提交! - 放环境变量?运维说他们只支持 K8s Secret,但测试环境还没上 K8s……
最后我们折中:开发环境用 application-dev.yml(加 .gitignore),生产环境通过启动参数传入:
java -jar app.jar --jwt.secret=$(cat /run/secrets/jwt_secret)
顺便吐槽一句:永远不要相信“临时方案”。这个“临时”的密钥方案,上线三个月了还没改。
代码可读性救了我一命
因为注重可维护性,我把认证逻辑拆得很清楚:
JwtUtil:负责生成/解析 TokenUserDetailsServiceImpl:从 DB 加载用户(支持手机号/邮箱登录)JwtAuthenticationFilter:拦截请求,提取 Token 并构建 AuthenticationSecurityConfig:配置链路
这样即使后来同事接手,也能快速看懂。反观隔壁组那个把所有逻辑塞在一个 @Component 里的“天才”,上线后连他自己都改不动了。
前端联调:跨域问题差点让我辞职
你以为配好后端就完了?天真!前端本地开发用 http://localhost:3000,后端在 http://localhost:8080,浏览器直接给你一个 CORS error。
Spring Security 的 CORS 配置必须 在 authorizeHttpRequests 之前声明,否则 OPTIONS 请求会被当成未认证直接拦掉:
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(...)
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("http://localhost:*", "https://*.ourapp.com"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(true); // 注意:如果用了 credentials,allowed origins 不能是 *
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
重点:setAllowedOriginPatterns 要用 * 的话必须配合 setAllowCredentials(false),但我们用了 Cookie(虽然 STATELESS 但前端想存 refresh token),所以只能写死域名模式。
性能考虑:别让认证成为瓶颈
在京东时,我们双11前都会压测认证接口。这次虽然流量不大,但我还是做了两点优化:
- Redis 缓存用户信息:避免每次请求都查 DB
- Token 黑名单延迟失效:登出时不立即删 Token,而是等它自然过期(配合 short-lived access token + long-lived refresh token)
// 登出示例:只把 refresh token 加入黑名单,access token 任其过期
redisTemplate.opsForValue().set(
"blacklist:refresh:" + refreshToken,
"true",
Duration.ofHours(7 * 24) // refresh token 有效期7天
);
这样既保证安全性,又减少 Redis 写压力。
最后说两句
折腾两周,终于跑通了。新前端能登录,老 Python 服务也能验签,测试小姐姐也没再提 bug。
回头想想,Spring Security 其实不难,难的是 理解它的 Filter Chain 执行顺序 和 新版 API 的变化。网上很多教程还是基于 Spring Boot 2.x 的,照搬肯定翻车。
如果你也在新项目里踩坑,记住三点:
- 别信旧文档,先看官方 Migration Guide
- 联调时打开浏览器 Network 面板,看清楚到底是 401 还是 403
- 和前端约好 Header、Body 格式,写进 Confluence,别靠口头约定
对了,听说下个月还要集成 OAuth2.0 支持微信登录…… 我已经开始肝了。要是再翻车,可能真得回京东搬砖了 😅
本文代码已脱敏,核心逻辑可复用。
环境:Spring Boot 3.2 + Spring Security 6.2 + JDK 17
作者:前京东后端,现某“高速成长”公司打工人,重度依赖 AI 但坚持手写注释

评论 0