35岁还在写代码的老程序员:用 Spring Security 搞定认证,顺便准备跳槽

老板说加个AI
2026-04-30 22:36
阅读 907

上周五晚上十一点,我一边啃着冷掉的黄焖鸡,一边盯着 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%。

后来优化成两步:

  1. 登录时一次性加载用户所有权限,缓存在 Redis,Key 是 user:permissions:{userId},Value 是权限字符串集合(如 ["user:read", "order:create"])。
  2. JWT Token 中只存 userId,不存权限(避免 Token 过大且权限变更后无法及时失效)。
  3. 自定义 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 挂了,你的系统会不会裸奔?

我的回答思路:

  1. 动态刷新:除了 Redis 缓存,我在用户权限变更时,向 Kafka 发一条事件,所有服务实例消费后清除本地缓存(Caffeine)+ Redis 缓存。双重保障。
  2. JWT 防重放:Token 中加入 jti(JWT ID),并设置短有效期(15分钟)。同时用 Redis 记录已使用的 jti,TTL 略大于 Token 有效期。虽然增加一次 Redis 调用,但安全值得。
  3. 降级策略:Redis 不可用时,自动 fallback 到数据库实时查询(带熔断机制,比如 Hystrix 或 Resilience4j),并告警通知。宁可慢一点,也不能没权限控制。

这些设计不一定完美,但至少说明你考虑过生产环境的复杂性,而不是只会跑通 demo。


最后一点真心话

35 岁还在写代码,说实话有点焦虑。但焦虑没用,不如多写点能放进简历的硬核东西。Spring Security 看似基础,但深挖下去,涉及密码学、缓存、分布式、安全攻防,全是加分项。

我也看过 GitHub 上那些 Go 写的 auth 服务,简洁漂亮。但现实是,大多数公司还在用 Java 维护老系统。能把 Spring Security 用好、调优、讲清楚,比盲目追新更重要。

对了,我把这套认证模块抽成了 starter,放到公司内部 GitLab 了(没敢开源,怕被喷)。如果你也在折腾类似的东西,欢迎交流——说不定下次跳槽,我们能在新公司当同事。

毕竟,谁不想找个不用半夜修 403 的班呢?

评论 0

最热最新
暂无评论
老板说加个AILv.1
0
影响力
0
文章
0
粉丝