【Spring Security】Spring Security 密码编辑器

Spring Security

Spring Security 密码编辑器

Maven 依赖

Maven 版本管理

Spring Security 和 Spring Framework 是 独立版本体系

Spring Security 示例版本:

xml 复制代码
<spring.security.version>5.3.4.RELEASE</spring.security.version>

Spring Framework 示例版本:

xml 复制代码
<spring.version>5.2.8.RELEASE</spring.version>

注意:Spring Security 与 Spring Framework 版本不必一致,但需要兼容。一般查看官方兼容矩阵即可。

Maven 依赖示例(传统 Spring)

典型 POM.XML 结构:

xml 复制代码
<dependencies>
    <!-- Spring MVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <!-- Spring Security 核心模块 -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>${spring.security.version}</version>
    </dependency>

    <!-- Servlet API -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>

    <!-- JSTL -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>
</dependencies>

这种方式适合传统 Spring MVC 项目,手动管理模块版本。

Spring Boot 集成(推荐方式)

Spring Boot 提供了 starter 依赖,自动引入所有核心和常用模块:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Boot 示例 POM:

xml 复制代码
<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Thymeleaf + Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity6</artifactId>
    </dependency>

    <!-- 测试支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

优点:

  1. 自动引入 core + web + config 等依赖;
  2. 版本统一管理,无需手动设置 spring-security.version
  3. 支持 Spring Boot 自动配置,快速集成安全功能。

依赖管理技巧

  1. 版本统一 :使用 <properties> 统一定义 spring.security.versionspring.version,方便升级。
  2. 按需引入扩展模块
    • LDAP 项目 → 添加 spring-security-ldap
    • SSO 项目 → 添加 spring-security-cas
    • OAuth2 项目 → 添加 spring-security-oauth2
  3. 避免冗余依赖
    • Web 项目无需引入 LDAP 或 ACL 模块,保持轻量化。
  4. 测试支持
    • 使用 spring-security-test 可轻松模拟用户认证、角色授权,便于单元测试。

密码编码器

Spring Security 5 之前,我们可以直接在内存中使用明文密码,比如:

java 复制代码
UserDetails user = User.builder()
    .username("user")
    .password("user123") // 明文
    .roles("USER")
    .build();

但是 Spring Security 5 之后,为了增强安全性:

  • 默认 不再支持明文密码
  • 所有密码都必须经过编码(加密)。
  • 如果你要使用明文密码,需要显式加上 {noop} 前缀:
java 复制代码
.password("{noop}user123") // 表示明文

这种做法仅用于开发或测试环境,生产环境绝对不能用。

NoOpPasswordEncoder

{noop} 对应的是 Spring 内置的 NoOpPasswordEncoder,它只是直接返回原始密码,不做加密。

java 复制代码
@Bean
protected UserDetailsService userDetailsService() {
    UserDetails user = User.builder()
        .username("user")
        .password("{noop}user123")
        .roles("USER")
        .build();
    return new InMemoryUserDetailsManager(user);
}
  • 优点:简单、方便测试。
  • 缺点:不安全、已废弃(deprecated),不适合生产环境。

推荐做法:使用密码编码器(PasswordEncoder)

Spring Security 提供了多种 密码编码器 ,最常用的是 BCryptPasswordEncoder

使用 BCryptPasswordEncoder 的示例:

java 复制代码
@Bean
protected PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
protected UserDetailsService userDetailsService() {
    UserDetails user = User.builder()
        .username("user")
        .password(passwordEncoder().encode("user123")) // 密码加密
        .roles("USER")
        .build();

    UserDetails admin = User.builder()
        .username("admin")
        .password(passwordEncoder().encode("admin123")) // 密码加密
        .roles("USER", "ADMIN")
        .build();

    return new InMemoryUserDetailsManager(user, admin);
}

测试案例:用户/管理员登录

  1. 引入 Spring Security、Thymeleaf 的相关依赖

    xml 复制代码
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-springsecurity6</artifactId>
    </dependency>
  2. src/main/resources/templates 目录下创建测试页面

    • 登录页(login.html

      html 复制代码
      <!DOCTYPE html>
      <html xmlns:th="http://www.thymeleaf.org">
      <head>
          <meta charset="UTF-8">
          <title>Login</title>
      </head>
      <body>
          <h1>登录</h1>
          
          <!-- 显示登录错误信息 -->
          <div th:if="${param.error}" style="color: red;">
              用户名或密码错误
          </div>
          
          <!-- 显示登出信息 -->
          <div th:if="${param.logout}" style="color: green;">
              已成功登出
          </div>
          
          <!-- 登录表单:Spring Security 会自动处理提交的 username 和 password -->
          <form th:action="@{/login}" method="post">
              <div>
                  <label>用户名:</label>
                  <input type="text" name="username" required>
              </div>
              <div>
                  <label>密码:</label>
                  <input type="password" name="password" required>
              </div>
              <div>
                  <button type="submit">登录</button>
              </div>
          </form>
      </body>
      </html>
    • 首页(home.html

      html 复制代码
      <!DOCTYPE html>
      <html xmlns:th="http://www.thymeleaf.org"
            xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
      <head>
          <meta charset="UTF-8">
          <title>首页</title>
      </head>
      <body>
          <h1>欢迎,<span sec:authentication="name"></span>!</h1> <!-- 显示当前用户名 -->
          <p>你的角色:<span sec:authentication="authorities"></span></p> <!-- 显示角色 -->
      
          <!-- 仅 ADMIN 可见的链接 -->
          <div sec:authorize="hasRole('ADMIN')">
              <a th:href="@{/admin}">管理员页面</a>
          </div>
      
          <!-- 登出链接 -->
          <form th:action="@{/logout}" method="post">
              <button type="submit">登出</button>
          </form>
      </body>
      </html>
    • 管理员页面(admin/admin.html

      ADMIN 角色可访问:

      html 复制代码
      <!DOCTYPE html>
      <html xmlns:th="http://www.thymeleaf.org">
      <head>
          <meta charset="UTF-8">
          <title>管理员页面</title>
      </head>
      <body>
          <h1>管理员专属页面</h1>
          <a th:href="@{/home}">返回首页</a>
      </body>
      </html>
  3. 创建相关配置类

    • PasswordConfig 密码加密配置类

      java 复制代码
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
      import org.springframework.security.crypto.password.PasswordEncoder;
      
      @Configuration
      public class PasswordConfig {
          @Bean
          protected PasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
      }
    • UserConfig 用户配置类

      java 复制代码
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.security.core.userdetails.User;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.crypto.password.PasswordEncoder;
      import org.springframework.security.provisioning.InMemoryUserDetailsManager;
      
      @Configuration
      public class UserConfig {
      
          // 注入 PasswordConfig 中定义的 PasswordEncoder
          private final PasswordEncoder passwordEncoder;
          public UserConfig(PasswordConfig passwordConfig) {
              this.passwordEncoder = passwordConfig.passwordEncoder();
          }
      
          @Bean
          protected UserDetailsService userDetailsService() {
              UserDetails user = User.builder()
                      .username("user")
                      .password(passwordEncoder.encode("user123")) // 密码加密
                      .roles("USER")
                      .build();
      
              UserDetails admin = User.builder()
                      .username("admin")
                      .password(passwordEncoder.encode("admin123")) // 密码加密
                      .roles("USER", "ADMIN")
                      .build();
      
              return new InMemoryUserDetailsManager(user, admin);
          }
      
      }
    • SecurityConfig 权限控制配置类

      java 复制代码
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
      import org.springframework.security.web.SecurityFilterChain;
      
      @Configuration
      @EnableWebSecurity
      public class SecurityConfig {
      
          // 注入 UserConfig 和 PasswordConfig 中定义的 PasswordEncoder 和 UserDetailsService
          private final UserConfig userConfig;
          private final PasswordConfig passwordConfig;
      
          public SecurityConfig(UserConfig userConfig, PasswordConfig passwordConfig) {
              this.userConfig = userConfig;
              this.passwordConfig = passwordConfig;
          }
      
      
          @Bean
          public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
              http
                      // 配置登录页和权限规则
                      .authorizeHttpRequests(auth -> auth
                              .requestMatchers("/login", "/css/**").permitAll() // 登录页和静态资源允许匿名访问
                              .requestMatchers("/admin/**").hasRole("ADMIN") // /admin/** 路径需要 ADMIN 角色
                              .anyRequest().authenticated() // 其他路径需要认证
                      )
                      // 配置登录页(Thymeleaf 页面)
                      .formLogin(form -> form
                              .loginPage("/login") // 自定义登录页路径
                              .defaultSuccessUrl("/home", true) // 登录成功后跳转首页
                              .failureUrl("/login?error=true") // 登录失败跳转
                      )
                      // 配置登出
                      .logout(logout -> logout
                              .logoutSuccessUrl("/login?logout=true") // 登出成功跳转
                      );
      
              return http.build();
          }
      }
    • 三个配置类也可以合为一个配置类:

      java 复制代码
      package com.scarletkite.springsecuritydemo.security;
      
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
      import org.springframework.security.core.userdetails.User;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
      import org.springframework.security.crypto.password.PasswordEncoder;
      import org.springframework.security.provisioning.InMemoryUserDetailsManager;
      import org.springframework.security.web.SecurityFilterChain;
      
      @Configuration
      @EnableWebSecurity
      public class SecurityConfig {
      
          // 1. 定义 PasswordEncoder Bean
          @Bean
          public PasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
      
          // 2. 定义 UserDetailsService Bean(直接调用当前类的 passwordEncoder() 方法)
          @Bean
          public UserDetailsService userDetailsService() {
              UserDetails user = User.builder()
                      .username("user")
                      .password(passwordEncoder().encode("user123")) // 直接调用本类的 passwordEncoder()
                      .roles("USER")
                      .build();
      
              UserDetails admin = User.builder()
                      .username("admin")
                      .password(passwordEncoder().encode("admin123")) // 直接调用本类的 passwordEncoder()
                      .roles("USER", "ADMIN")
                      .build();
      
              return new InMemoryUserDetailsManager(user, admin);
          }
      
          // 3. 删除构造器注入(不再需要,因为内部方法可直接调用)
      
          // 4. 定义 SecurityFilterChain Bean(配置安全规则)
          @Bean
          public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
              http
                      .authorizeHttpRequests(auth -> auth
                              .requestMatchers("/login", "/css/**").permitAll()
                              .requestMatchers("/admin/**").hasRole("ADMIN")
                              .anyRequest().authenticated()
                      )
                      .formLogin(form -> form
                              .loginPage("/login")
                              .defaultSuccessUrl("/home", true)
                              .failureUrl("/login?error=true")
                      )
                      .logout(logout -> logout
                              .logoutSuccessUrl("/login?logout=true")
                      );
      
              return http.build();
          }
      }
  4. 创建控制器(处理页面跳转)

    java 复制代码
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class PageController {
    
        // 登录页
        @GetMapping("/login")
        public String login() {
            return "login";
        }
    
        // 首页
        @GetMapping("/home")
        public String home() {
            return "home";
        }
    
        // 管理员页面
        @GetMapping("/admin")
        public String admin() {
            return "admin/admin";
        }
    }
  5. 在浏览器地址栏输入 localhost:8080/login 进行测试

BCrypt 密码结构解析

BCrypt 密码在内存中或数据库中存储的格式如下:

mathematica 复制代码
$2a$10$acaXGauv/3buNdwQWeOgu.iab3LLDclrH64xVMsSxd9Lp/otgUfMm

各字段含义:

字段 说明
2a 使用的编码算法,bcrypt
10 算法强度(strength,cost factor)
前 22 个字符 随机盐(salt)
后 31 个字符 哈希后的密码(hashed password)

BCrypt 的特点:

  • 每次编码都使用随机盐(salt)
  • 相同的明文密码每次编码结果都不同
  • 提供强抗破解能力,适合生产环境

使用场景

  1. 内存用户(InMemoryUserDetailsManager)

    • 对开发或测试环境,使用 BCryptPasswordEncoder 也可以。
  2. 数据库用户(Persistent storage)

    • 推荐在用户注册时先用 BCryptPasswordEncoder.encode() 加密密码,再存入数据库。

    • 登录验证时,Spring Security 会自动调用 matches() 方法比较原始密码和加密后的密码。

      java 复制代码
      passwordEncoder.matches(rawPassword, encodedPassword)

密码编码器代理

什么是 DelegatingPasswordEncoder?

DelegatingPasswordEncoderSpring Security 的密码编码器代理(Delegator)

它的作用是:

"根据密码前缀(id)动态选择合适的密码加密算法。"

比如数据库中存了三种不同类型的密码:

复制代码
{bcrypt}$2a$10$Fbp...
{noop}123456
{pbkdf2}9ddae64d...

Spring Security 看到 {bcrypt} 前缀,就自动使用 BCryptPasswordEncoder 去验证;

看到 {pbkdf2} 就用 PBKDF2 算法。

总结:

DelegatingPasswordEncoder = "密码算法调度中心",用来统一管理多种加密方式。

为什么需要它?

背景问题:Spring Security 5 之前,密码一般只用一种算法,比如:

java 复制代码
@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

但随着项目演进,你可能遇到这些问题:

  • 老系统用 MD5 或 SHA 存密码;
  • 新系统想改用更安全的 BCrypt;
  • 不同模块或第三方用户来源不同;
  • 想平滑过渡,不影响旧用户登录。

于是 Spring Security 引入了 DelegatingPasswordEncoder 来统一管理不同加密算法的兼容性与迁移。

内部原理结构

它的核心思想就是一个 Map + 默认算法 ID

java 复制代码
public class DelegatingPasswordEncoder implements PasswordEncoder {

    private final String idForEncode; // 当前默认算法
    private final Map<String, PasswordEncoder> idToPasswordEncoder; // 所有可用算法
    private final PasswordEncoder defaultPasswordEncoderForMatches; // 匹配时默认使用的

    // encode() 时加上 {id}
    // matches() 时根据 {id} 选择对应 encoder
}

Spring 提供了一个工厂方法:

java 复制代码
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

这行代码创建了一个默认配置的 DelegatingPasswordEncoder,支持以下算法:

ID 对应编码器
bcrypt BCryptPasswordEncoder
ldap LdapShaPasswordEncoder
MD4 Md4PasswordEncoder
MD5 MessageDigestPasswordEncoder
noop NoOpPasswordEncoder
pbkdf2 Pbkdf2PasswordEncoder
scrypt SCryptPasswordEncoder
sha256 StandardPasswordEncoder
argon2 Argon2PasswordEncoder

使用方式

  1. 创建并使用默认 DelegatingPasswordEncoder

    java 复制代码
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    它的默认算法是 {bcrypt}

    java 复制代码
    String encoded = passwordEncoder().encode("123456");
    // 输出类似:{bcrypt}$2a$10$NPMD...

    会发现,前缀 {bcrypt} 自动加上了

    验证时,Spring Security 会:

    1. 读取密码前缀 {bcrypt}
    2. 从内部 Map 查找对应编码器
    3. 调用 matches() 进行验证
  2. 自定义 DelegatingPasswordEncoder

    比如,想默认用 PBKDF2,但仍然支持旧的 BCrypt:

    java 复制代码
    @Bean
    public PasswordEncoder passwordEncoder() {
        String idForEncode = "pbkdf2";
    
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
    
        return new DelegatingPasswordEncoder(idForEncode, encoders);
    }

    效果:

    • 新注册用户 → PBKDF2 编码(前缀 {pbkdf2}
    • 老用户若用 {bcrypt} → 仍然能匹配
    • 临时测试账号可用 {noop} 明文密码
  3. 旧密码无前缀怎么办?

    如果数据库中的密码没有 {id} 前缀,比如旧系统存的是纯 BCrypt:

    可以设置一个默认匹配策略:

    java 复制代码
    DelegatingPasswordEncoder delegatingPasswordEncoder =
        (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
    
    // 设置一个默认匹配器,当没有前缀时使用 bcrypt
    delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());

工作流程图

mathematica 复制代码
       ┌──────────────┐
       │ encode("123")│
       └──────┬───────┘
              ↓
   +--------------------------+
   | DelegatingPasswordEncoder|
   +--------------------------+
   | idForEncode = "bcrypt"   |
   | idToPasswordEncoder = {  |
   |   bcrypt → BCryptEncoder |
   |   pbkdf2 → Pbkdf2Encoder |
   |   noop → NoOpEncoder     |
   | }                        |
   +--------------------------+
              ↓
     输出 {bcrypt}$2a$10$...

验证过程:

mathematica 复制代码
matches(raw, "{bcrypt}$2a$10$...")
→ 提取 {bcrypt}
→ 找出 BCryptPasswordEncoder
→ 调用 matches()
→ 返回 true / false

前面的 PasswordConfig 配置类可以改为:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class PasswordConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        String idForEncode = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
        encoders.put("argon2", new Argon2PasswordEncoder(10, 8, 1, 16, -1));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());

        DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
        passwordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
        return passwordEncoder;
    }

}

DelegatingPasswordEncoder 是 Spring Security 密码策略的核心。

它让我们可以:

  • 安全地管理多种密码算法;
  • 平滑升级加密方案;
  • 保持老用户可登录;
  • 确保系统的密码验证逻辑统一、可扩展、可演化。
相关推荐
MZ_ZXD00126 分钟前
springboot旅游信息管理系统-计算机毕业设计源码21675
java·c++·vue.js·spring boot·python·django·php
PP东28 分钟前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
ManThink Technology34 分钟前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
invicinble38 分钟前
springboot的核心实现机制原理
java·spring boot·后端
人道领域1 小时前
SSM框架从入门到入土(AOP面向切面编程)
java·开发语言
大模型玩家七七1 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
CodeToGym2 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel
凡人叶枫2 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发
JMchen1232 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
阔皮大师2 小时前
INote轻量文本编辑器
java·javascript·python·c#