裸辞半年后,我用 Spring Security 三天搞定登录认证

Node不想睡
2025-12-29 03:58
阅读 625

去年八月,我裸辞了。不是因为受不了产品经理的“这个需求很简单”,也不是因为运维半夜把我叫起来修线上事故(虽然这种事确实发生过三次),纯粹是想停下来喘口气,顺便把落下的技术债补一补。Gap 的这半年,我刷 LeetCode、学 Rust、甚至试着用 Python 写了个小爬虫去 GitHub 上抓开源项目的 star 趋势——结果发现大家最爱的还是 vuereact,Java 项目居然排不进前十,心酸。

但现实很骨感。十月投简历时,HR 一句“你半年没写代码了?”直接让我冷汗直流。为了证明自己没躺平,我在 GitHub 上建了个新仓库,名字就叫 auth-demo,目标很朴素:用 Spring Boot + Spring Security 快速搭一个带登录认证的系统。没想到,这个 demo 后来成了我面试时的救命稻草。

如今入职新公司两个月,团队在做一个 B 端 SaaS 平台,正好要重构权限模块。领导看我简历里写了“熟悉安全框架”,二话不说把任务派给了我。得,demo 变生产,压力山大。好在 Spring Security 虽然配置复杂到让人想哭,但只要摸清套路,其实比调 CSS 动画还简单(别笑,前端同事总说我这句话是在凡尔赛)。


为什么不用 OAuth2 或 JWT?先搞清楚场景

很多教程一上来就教你怎么集成微信登录、怎么签发 JWT Token,搞得好像不用这些就是技术落后。但咱们做的是内部管理系统,用户都是企业员工,压根不需要第三方授权。老板只关心两件事:能不能快速登录?会不会被黑客拖库?

所以,我们选择了最经典的基于 Session 的表单认证(Form Login)。它天然支持 CSRF 防护、会话管理、登出清除 Cookie,而且调试起来比 JWT 直观多了——至少浏览器 DevTools 里能看到 JSESSIONID 变化,不像 JWT 一串 base64 看得人眼花。

当然,如果你要做前后端分离或者移动端 API,JWT 更合适。但对我们这种传统 Web 应用,Session 就够了,别过度设计。


三步走:从零搭建安全骨架

第一步:依赖别乱加

Spring Security 的 starter 包其实很智能,但很多人一上来就复制网上的 pom.xml,结果引入一堆用不到的模块。我们的原则是:最小必要依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 如果用 Thymeleaf 渲染页面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

注意:不要手动加 spring-security-corespring-security-config,starter 已经包含它们了。我见过有同事因为多加了一个旧版本依赖,导致 PasswordEncoder 注入失败,debug 到凌晨两点。


第二步:配置类是灵魂

Spring Security 的核心是 WebSecurityConfigurerAdapter(虽然在新版本中已废弃,但为了兼容性我们暂时还在用)。我的配置类长这样:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login", "/css/**", "/js/**").permitAll() // 静态资源放行
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")           // 自定义登录页
                .defaultSuccessUrl("/dashboard") // 登录成功跳转
                .failureUrl("/login?error")     // 登录失败重定向
                .permitAll()
            .and()
            .logout()
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)    // 清除 session
                .deleteCookies("JSESSIONID")    // 删除 cookie
                .permitAll()
            .and()
            .csrf().disable(); // 开发环境暂时关闭,生产务必开启!
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

这里有几个坑必须提:

  • 静态资源路径一定要放行,否则登录页的 CSS 加载不出来,UI 同事会找你拼命。
  • CSRF 默认开启,但如果你用的是纯 AJAX 前端(比如 Vue),可能需要额外处理 _csrf token。我们用的是服务端渲染,所以暂时关掉方便调试,上线前一定记得打开。
  • 密码必须加密!别再用明文存数据库了,BCrypt 是行业标配。

第三步:用户数据从哪来?

Spring Security 不关心你用户存在 MySQL 还是 MongoDB,它只认 UserDetailsService 接口。我写了个简单的实现:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return org.springframework.security.core.userdetails.User
                .builder()
                .username(user.getUsername())
                .password(user.getPassword()) // 数据库存的已经是 BCrypt 加密后的
                .roles("USER")
                .build();
    }
}

数据库设计也很简单:

字段 类型 说明
id BIGINT 主键
username VARCHAR(50) 唯一
password VARCHAR(100) BCrypt 加密后长度约60
enabled BOOLEAN 是否启用

注意:不要在数据库里存原始密码!曾经有个实习生这么干,被安全团队扫出来后,整个组都被通报批评了。


生产环境踩过的坑

1. 登录页无限重定向

现象:访问 /login,结果被重定向到 /login?error,再刷新又跳回 /login,死循环。

原因:loginPage("/login") 指向的路径本身被 security 拦截了!解决方案是在 antMatchers 里明确放行 /login

2. 登出后还能访问页面

用户点了“退出”,URL 跳转了,但按浏览器后退键居然还能看到上一页内容。

这是因为浏览器缓存了页面。解决方法是在 Controller 里加响应头:

@GetMapping("/dashboard")
public String dashboard(HttpServletRequest request, HttpServletResponse response) {
    response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    response.setHeader("Pragma", "no-cache");
    response.setDateHeader("Expires", 0);
    return "dashboard";
}

3. 密码加密方式不一致

本地测试用 BCryptPasswordEncoder,但生产环境部署时,运维用脚本初始化用户,直接插了明文密码。结果所有用户都登录不了。

后来我们统一了初始化流程:写一个 CommandLineRunner,启动时自动创建 admin 用户,并用相同的 encoder 加密。


和 Python 项目的对比思考

有趣的是,在 Gap 期间我用 Flask + Flask-Login 也做过类似的认证系统。对比下来:

维度 Spring Security Flask-Login
学习曲线 陡峭(配置复杂) 平缓(几行代码)
安全性 开箱即用(CSRF、Session Fixation 防护) 需手动配置
扩展性 插件丰富(OAuth2、LDAP 等) 依赖社区扩展
调试体验 报错信息晦涩 直接看源码,简单明了

说白了,Spring Security 是“重型武器”,适合企业级应用;Flask-Login 更像“瑞士军刀”,适合快速原型。我在 GitHub 上那个 Python 项目现在只有 12 个 star,而 Java 的 auth-demo 居然有 37 个——看来还是 Java 老铁们更爱折腾安全框架啊。


最后一点真心话

说实话,刚接手这个任务时我是抗拒的。毕竟 Gap 半年,连 IDEA 快捷键都快忘了。但真正动手后发现,Spring Security 虽然文档又臭又长,但只要抓住几个核心概念(Authentication、Authorization、Filter Chain),其实逻辑很清晰。

现在系统上线两周,0 安全事故,产品经理也没来找茬。上周五下班前,领导还拍了拍我肩膀说:“小张,干得不错。”那一刻,突然觉得裸辞后的焦虑值回票价了。

如果你也在准备面试,或者刚入职新公司被塞了个“安全模块”的活儿,别慌。从最简单的表单登录开始,一步步来。GitHub 上搜 spring-security-example,有成千上万的参考项目。我那个 demo 仓库也开源了,欢迎提 issue 吐槽——毕竟,程序员最好的成长方式,就是把坑踩给别人看(不是)。

共勉。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝