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>
优点:
- 自动引入
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 之前,我们可以直接在内存中使用明文密码,比如:
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);
}
测试案例:用户/管理员登录
-
引入 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> -
在
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>
-
-
创建相关配置类
-
PasswordConfig 密码加密配置类
javaimport 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 用户配置类
javaimport 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 权限控制配置类
javaimport 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(); } } -
三个配置类也可以合为一个配置类:
javapackage 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(); } }
-
-
创建控制器(处理页面跳转)
javaimport 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 密码在内存中或数据库中存储的格式如下:
mathematica
$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()方法比较原始密码和加密后的密码。javapasswordEncoder.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 之前,密码一般只用一种算法,比如:
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 |
使用方式
-
创建并使用默认 DelegatingPasswordEncoder
java@Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }它的默认算法是
{bcrypt}。javaString encoded = passwordEncoder().encode("123456"); // 输出类似:{bcrypt}$2a$10$NPMD...会发现,前缀
{bcrypt}自动加上了验证时,Spring Security 会:
- 读取密码前缀
{bcrypt} - 从内部 Map 查找对应编码器
- 调用
matches()进行验证
- 读取密码前缀
-
自定义 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}明文密码
- 新注册用户 → PBKDF2 编码(前缀
-
旧密码无前缀怎么办?
如果数据库中的密码没有
{id}前缀,比如旧系统存的是纯 BCrypt:可以设置一个默认匹配策略:
javaDelegatingPasswordEncoder 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 密码策略的核心。
它让我们可以:
- 安全地管理多种密码算法;
- 平滑升级加密方案;
- 保持老用户可登录;
- 确保系统的密码验证逻辑统一、可扩展、可演化。