快速搭建 Spring Security 认证系统:一个 DevOps 工程师的实战笔记
上周五晚上十点,我正一边远程办公,一边撸着 Go 写的一个小工具(没错,虽然主业 Java,但业余时间总得搞点新花样),突然 Slack 弹出一条消息——产品经理说:“我们下周要上线一个内部管理平台,必须带登录认证,不能裸奔。” 我当时就笑了:裸奔?那可不是咱们干的事。
作为一个常年混迹开源项目源码、对底层原理有点执念的 DevOps 工程师,我其实早就在等这么个机会:把一套干净、安全、可扩展的身份认证系统快速搭起来,顺便给团队留个“样板工程”。于是,周末两天没出门,泡了三壶咖啡,硬是用 Spring Security v0(准确说是基于 Spring Boot 3.x + Spring Security 6.x)整了一套最小可行的安全体系。今天就来聊聊这个过程中的思路、踩坑和收获。
背景:不是所有需求都值得从零造轮子
说实话,我一度想直接上 Keycloak 或 Auth0。但这次是个轻量级内部系统,用户量不大,团队也没运维复杂认证服务的经验。更重要的是——Deadline 就在眼前。这时候,Spring Security 的“开箱即用”特性就显得格外香。
不过别误会,我不是那种“加个 starter 就完事”的人。我喜欢看源码,喜欢知道每行配置背后发生了什么。比如 WebSecurityConfigurerAdapter 在 Spring Security 6 里被移除了,很多老教程直接废掉。这让我想起去年双11前,因为用了过时的配置导致 OAuth2 流程崩了,差点被叫去“喝茶”。
所以这次,我决定从最基础的表单登录开始,一步步构建,确保每一步都可控、可观测、可运维。
核心目标:安全、简洁、可扩展
我的设计原则很简单:
- 默认安全:不用写一堆配置就能防常见攻击(CSRF、会话固定等)
- 开发友好:本地调试方便,日志清晰
- 生产就绪:支持后续无缝接入 JWT、OAuth2 或 LDAP
- 不依赖前端:后端独立提供完整认证能力
为此,我选了以下技术栈组合:
| 组件 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 3.2.x | 使用 Jakarta EE 9+,包名从 javax.* 变成 jakarta.* |
| Spring Security | 6.2.x | 安全核心,v0 级别的起步 |
| Spring Data JPA | 3.2.x | 用户数据持久化 |
| H2 / PostgreSQL | - | 开发用 H2,生产切 PG |
| BCrypt | 内置 | 密码加密 |
注:这里说的 “v0” 并非官方版本号,而是指“从零开始的第一版”,类似 MVP(Minimum Viable Product)的概念。顺便说一句,Go 社区也常用 v0 表示初始原型,虽然语言不同,但工程师思维相通。
实战:三步搞定基础认证
第一步:定义用户模型与存储
先建个 User 实体,关键字段不能少:
@Entity
@Table(name = "users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password; // 存储 BCrypt 加密后的字符串
private boolean enabled = true;
private boolean accountNonExpired = true;
private boolean credentialsNonExpired = true;
private boolean accountNonLocked = true;
}
注意:Spring Security 要求实现 UserDetails 接口。所以我让 AppUser 不直接实现,而是封装一个 UserDetails 的适配器:
public class SecurityUser implements UserDetails {
private final AppUser user;
public SecurityUser(AppUser user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 简化:默认 ROLE_USER
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// 其他 isXXX 方法直接返回 user 的对应字段
}
这样既解耦了业务模型和安全框架,又保留了灵活性。
第二步:配置 SecurityFilterChain(告别 WebSecurityConfigurerAdapter!)
这是最容易翻车的地方。很多人还在搜旧教程,结果项目启动就报错。正确的姿势是用 @Bean 声明 SecurityFilterChain:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/error", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页(可选)
.defaultSuccessUrl("/dashboard", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.csrf(csrf -> csrf.disable()) // 开发阶段可关,生产务必开启!
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(UserRepository userRepo) {
return username -> {
AppUser user = userRepo.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new SecurityUser(user);
};
}
}
重点来了:
csrf().disable():仅用于开发或无状态 API。如果你做的是传统 Web 应用,千万别关!否则会被安全审计打脸。UserDetailsService:这是 Spring Security 加载用户的入口,务必自己实现,别用内存用户(除非 demo)。- 密码编码器:BCrypt 是标配,别再存明文或 MD5 了,真的。
第三步:处理登录页面与错误
Spring Security 默认会跳转到 /login 并渲染一个简单表单。你可以自己写 Thymeleaf 页面,也可以前后端分离——这时候就需要自定义失败/成功处理器。
比如,返回 JSON 而不是重定向:
.formLogin(form -> form
.successHandler((request, response, authentication) -> {
response.setStatus(HttpStatus.OK.value());
response.getWriter().write("{\"status\":\"ok\"}");
})
.failureHandler((request, response, exception) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("{\"error\":\"" + exception.getMessage() + "\"}");
})
)
提醒:生产环境一定要记录登录失败日志,并考虑防暴力破解(比如失败5次锁定10分钟)。我们团队就吃过亏——测试同学跑自动化脚本疯狂试密码,触发了风控,结果真用户登不上,半夜被 PagerDuty 叫醒。
运维视角:如何让这套系统“活”下去?
作为 DevOps,我关心的不只是代码跑起来,更是它在线上怎么表现。
- 日志埋点:在
AuthenticationSuccessHandler和AuthenticationFailureHandler里打结构化日志,方便 ELK 分析。 - 会话管理:通过
SessionRegistry监控在线用户,必要时踢人。 - 健康检查:暴露
/actuator/health,集成 Spring Boot Actuator。 - 安全头:Spring Security 默认加了
X-Content-Type-Options,Strict-Transport-Security等,但建议显式配置:
http.headers(headers -> headers
.frameOptions().deny()
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
);
- 密钥轮换:虽然 BCrypt 不需要密钥,但如果后续引入 JWT,记得规划好密钥更新机制。
总结:v0 不是终点,而是起点
这套系统花了不到一天搭完,第二天就交付给了前端同事联调。他们惊讶于“居然不用我们处理 token 刷新”,而测试同学也很快跑通了登录流程。
但我知道,这只是一个 v0。真正的挑战在后面:多租户支持、社交登录、MFA、审计日志……不过没关系,有了这个坚实的基础,后续迭代就从容多了。
顺便说一句,写这篇文章的时候,我又切回 Go 写了个小工具来批量生成用户测试数据——有时候,跨语言的工具链反而能提升效率。毕竟,工程师的本质不是“用什么语言”,而是“解决问题”。
最后送大家一句话:安全不是功能,而是基础设施。 别等到被黑了才想起 Spring Security。
作者:一个在家远程办公、喜欢扒源码、偶尔用 Go 撸脚本的 DevOps 工程师。
项目代码已开源(脱敏版),欢迎 Star & PR。
下期预告:《Spring Security + JWT + Redis 实现分布式会话管理》

评论 0