裸辞半年后,我用 Spring Security 三天搞定登录认证
去年八月,我裸辞了。不是因为受不了产品经理的“这个需求很简单”,也不是因为运维半夜把我叫起来修线上事故(虽然这种事确实发生过三次),纯粹是想停下来喘口气,顺便把落下的技术债补一补。Gap 的这半年,我刷 LeetCode、学 Rust、甚至试着用 Python 写了个小爬虫去 GitHub 上抓开源项目的 star 趋势——结果发现大家最爱的还是 vue 和 react,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-core 或 spring-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),可能需要额外处理
_csrftoken。我们用的是服务端渲染,所以暂时关掉方便调试,上线前一定记得打开。 - 密码必须加密!别再用明文存数据库了,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