开发者

Spring Security密码编辑器示例详解

开发者 https://www.devze.com 2025-10-25 10:29 出处:网络 作者: 熬夜超级玩家
目录Spring Security 密码编辑器Maven 依赖密码编码器密码编码器代理Spring Security 密码编辑器
目录
  • Spring Security 密码编辑器
    • Maven 依赖
    • 密码编码器
    • 密码编码器代理

Spring Security 密码编辑器

Maven 依赖

Maven 版本管理

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

Spring Security 示例版本:

<spring.security.version>5.3.4.RELEASE</spring.security.version>

Spring Framework 示例版本:

<spring.version>5.2.8.RELEASE</spring.version>

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

Maven 依赖示例(传统 Spring)

典型 POM.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 依赖,自动引入所有核心和常用模块:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Boot 示例 POM:

<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 之前,我们可以直接在内存中使用明文密码,比如:

UserDetails user = User.builder()
    .username("user")
    .password("user123") // 明文
    .roles("USER")
    .build();

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

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

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

NoOpPasswordEncoder

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

@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 的示例:

@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);
}

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

引入 Spring Security、Thymeleaf 的相关依赖

<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>

src/main/resources/templates 目录下创建测试页面

登录页(login.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}">
        用户名或密码错误
    </div>
    <!-- 显示登出信息 -->
    <div th:if="${param.logout}">
        已成功登出
    </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

<!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="authoritijses"></span></p> <!-- 显示角色 -->
    <!-- 仅 ADMIN 可见的链接 -->
    <div sec:authorize="hasRole('ADMIN')">
        <a th:href="@{/admin}" rel="external nofollow" >管理员页面</a>
    </div>
    <!-- 登出链接 -->
    <form th:action="@{/logout}" method="post">
        <button type="submit">登出</button>
    </form>
</body>
</html>

管理员页面(admin/admin.html

ADMIN 角色可访问:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>管理员页面</title>
</head>
<body>
    <h1>管理员专属页面</h1>
    <a th:href="@{/home}" rel="external nofollow" >返回首页</a>
</body>
</html>

创建相关配置类

PasswordConfig 密码加密配置类

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 用户配置类

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 权限控制配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Config编程客栈uration;
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();
    }
}

三个配置类也可以合为一个配置类:

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.cryptojs.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();
    }
}

创建控制器(处理页面跳转)

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";
    }
}

在浏览器地址栏输入 localhost:8080/login 进行测试

BCrypt 密码结构解析

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

$2a$10$acaXGauv/3buNdwQWeOgu.iab3LLDclrH64xVMsSxd9Lp/otgUfMm

各字段含义:

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

BCrypt 的特点:

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

使用场景

  • 内存用户(InMemoryUserDetailsManager)
    • 对开发或测试环境,使用 BCryptPasswordEncoder 也可以。
  • 数据库用户(Persistent storage)
    • 推荐在用户注册时先用 BCryptPasswordEncoder.encode() 加密密码,再存入数据库。
    • 登录验证时,Spring Security 会自动调用 matches() 方法比较原始密码和加密后的密码。
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 之前,密码一般只用一种算法,比如:

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

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

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

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

内部原理结构

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

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 提供了一个工厂方法:

PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

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

ID对应编码器
bcryptBCryptPasswordEncoder
ldapLdapShaPasswordEncoder
MD4Md4PasswordEncoder
MD5MessageDigestPasswordEncoder
noopNoOpPasswordEncoder
pbkdf2Pbkdf2PasswordEncoder
scryptSCryptPasswordEncoder
sha256StandardPasswordEncoder
argon2Argon2PasswordEncoder

使用方式

创建并使用默认 DelegatingPasswordEncoder

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

它的默认算法是 {bcrypt}

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

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

验证时,Spring Security 会:

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

自定义 DelegatingPasswordEncoder

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

@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} 明文密码

旧密码无前缀怎么办?

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

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

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

工作流程图

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

验证过程:

matches(raw, "{bcrypt}$2a$10$...")

→ 提取 {bcrypt}

→ 找出 BCryptPasswordEncoder

→ 调用 matches()

→ 返回 true / false

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

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.BCryptPasswordEncodandroider;
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 密码策略的核心。

它让我们可以:

  • 安全地管理多种密码算法;
  • 平滑升级加密方案;
  • 保持老用户可登录;
  • 确保系统的密码验证逻辑统一、可扩展、可演化。

到此这篇关于Spring Security 密码编辑器的文章就介绍到这了,更多相关Spring Security 密码内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号