【Spring Security】Spring Security 密码编辑器
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>
优点:
- 自动引入
core + web + config
等依赖; - 版本统一管理,无需手动设置
spring-security.version
; - 支持 Spring Boot 自动配置,快速集成安全功能。
依赖管理技巧
- 版本统一:使用
<properties>
统一定义spring.security.version
和spring.version
,方便升级。 - 按需引入扩展模块:
- LDAP 项目 → 添加
spring-security-ldap
- SSO 项目 → 添加
spring-security-cas
- OAuth2 项目 → 添加
spring-security-oauth2
- LDAP 项目 → 添加
- 避免冗余依赖:
- Web 项目无需引入 LDAP 或 ACL 模块,保持轻量化。
- 测试支持:
- 使用
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}" 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
)<!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
角色可访问:<!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>
-
-
创建相关配置类
-
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 {@Beanprotected 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 中定义的 PasswordEncoderprivate final PasswordEncoder passwordEncoder;public UserConfig(PasswordConfig passwordConfig) {this.passwordEncoder = passwordConfig.passwordEncoder();}@Beanprotected 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.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 和 UserDetailsServiceprivate final UserConfig userConfig;private final PasswordConfig passwordConfig;public SecurityConfig(UserConfig userConfig, PasswordConfig passwordConfig) {this.userConfig = userConfig;this.passwordConfig = passwordConfig;}@Beanpublic 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.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain;@Configuration @EnableWebSecurity public class SecurityConfig {// 1. 定义 PasswordEncoder Bean@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 2. 定义 UserDetailsService Bean(直接调用当前类的 passwordEncoder() 方法)@Beanpublic 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(配置安全规则)@Beanpublic 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?
DelegatingPasswordEncoder
是 Spring 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 | 对应编码器 |
---|---|
bcrypt | BCryptPasswordEncoder |
ldap | LdapShaPasswordEncoder |
MD4 | Md4PasswordEncoder |
MD5 | MessageDigestPasswordEncoder |
noop | NoOpPasswordEncoder |
pbkdf2 | Pbkdf2PasswordEncoder |
scrypt | SCryptPasswordEncoder |
sha256 | StandardPasswordEncoder |
argon2 | Argon2PasswordEncoder |
使用方式
-
创建并使用默认 DelegatingPasswordEncoder
@Bean public PasswordEncoder passwordEncoder() {return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
它的默认算法是
{bcrypt}
。String encoded = passwordEncoder().encode("123456"); // 输出类似:{bcrypt}$2a$10$NPMD...
会发现,前缀
{bcrypt}
自动加上了验证时,Spring Security 会:
- 读取密码前缀
{bcrypt}
- 从内部 Map 查找对应编码器
- 调用
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}
明文密码
- 新注册用户 → PBKDF2 编码(前缀
-
旧密码无前缀怎么办?
如果数据库中的密码没有
{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.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 {@Beanpublic 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 密码策略的核心。
它让我们可以:
- 安全地管理多种密码算法;
- 平滑升级加密方案;
- 保持老用户可登录;
- 确保系统的密码验证逻辑统一、可扩展、可演化。