快速搭建 Spring Security 认证系统:一个 DevOps 工程师的实战笔记

半夜改Bug
2026-04-27 14:36
阅读 2056

上周五晚上十点,我正一边远程办公,一边撸着 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,我关心的不只是代码跑起来,更是它在线上怎么表现。

  1. 日志埋点:在 AuthenticationSuccessHandlerAuthenticationFailureHandler 里打结构化日志,方便 ELK 分析。
  2. 会话管理:通过 SessionRegistry 监控在线用户,必要时踢人。
  3. 健康检查:暴露 /actuator/health,集成 Spring Boot Actuator。
  4. 安全头:Spring Security 默认加了 X-Content-Type-Options, Strict-Transport-Security 等,但建议显式配置:
http.headers(headers -> headers
    .frameOptions().deny()
    .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
);
  1. 密钥轮换:虽然 BCrypt 不需要密钥,但如果后续引入 JWT,记得规划好密钥更新机制。

总结:v0 不是终点,而是起点

这套系统花了不到一天搭完,第二天就交付给了前端同事联调。他们惊讶于“居然不用我们处理 token 刷新”,而测试同学也很快跑通了登录流程。

但我知道,这只是一个 v0。真正的挑战在后面:多租户支持、社交登录、MFA、审计日志……不过没关系,有了这个坚实的基础,后续迭代就从容多了。

顺便说一句,写这篇文章的时候,我又切回 Go 写了个小工具来批量生成用户测试数据——有时候,跨语言的工具链反而能提升效率。毕竟,工程师的本质不是“用什么语言”,而是“解决问题”。

最后送大家一句话:安全不是功能,而是基础设施。 别等到被黑了才想起 Spring Security。


作者:一个在家远程办公、喜欢扒源码、偶尔用 Go 撸脚本的 DevOps 工程师。
项目代码已开源(脱敏版),欢迎 Star & PR。
下期预告:《Spring Security + JWT + Redis 实现分布式会话管理》

评论 0

最热最新
暂无评论
半夜改BugLv.1
0
影响力
0
文章
0
粉丝