一、是什么
- Spring Security 能解决什么问题?
用户身份认证(Authentication)和用户授权 (Authorization 也叫访问控制)。
认证:解决我是谁?
授权:解决我能在系统中干什么?
- 基本原理
Spring Security 本质是一个过滤器链。但是 Spring Security 采用了有别于传统 Servlet 过滤器链的另一套实现。
左边是传统的 Servlet 过滤器链 FilterChain,右边是 Spring Security 实现的过滤器链 SecurityFilterChain。
- FilterChain
客户端向应用程序发送请求,Servlet 容器创建一个 FilterChain,其中包含 Filters and Servlet,Filters and Servlet 根据请求 URI 的路径处理 HttpServletRequest,在 Spring MVC 应用程序中,Servlet 是 DispatcherServlet 的实例。最多一个 Servlet 可以处理单个 HttpServletRequest 和 HttpServletResponse。
- FilterChainProxy
在 Servlet 容器中,仅根据 URL 调用 Filter。但是,FilterChainProxy 可以通过利用RequestMatcher 接口,根据 HttpServletRequest 中的任何内容(请求头、请求路径、请求参数)来确定调用。FilterChainProxy 可用于确定应该使用哪个 SecurityFilterChain。
二、快速开始
2.1、hello world
从 官网 下载一个 hello-security demo, 或者 新建一个 Spring Boot 项目,引入依赖也可以。
2.1.1、引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
依赖的版本由 Spring Boot 去指定。
我当前用到的版本如下:
JDK 17
<spring-security.version>6.4.3</spring-security.version>
<spring-boot-starter-web.version>3.4.3</spring-boot-starter-web.version>
2.1.2、新建 Controller(表示我们要访问的资源)
@RestController
@RequestMapping("/res")
public class ResourceController {
@RequestMapping("/echo")
public String echo() {
return "Hello Security!";
}
}
2.1.3、日志配置
新版 Spring Security 默认是不打印 Security Filters 过滤器,可以通过如下方式打印:
修改项目日志配置文件 logback.xml 或 logback-spring.xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.security" level="DEBUG"/>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
将 org.springframework.security 包的日志级别设置为 DEBUG 或 TRACE
就可以在控制台看到
2025-04-09 17:15:57 - Will secure any request with filters:
DisableEncodeUrlFilter,
WebAsyncManagerIntegrationFilter,
SecurityContextHolderFilter,
HeaderWriterFilter,
CsrfFilter,
LogoutFilter,
UsernamePasswordAuthenticationFilter,
DefaultResourcesFilter,
DefaultLoginPageGeneratingFilter,
DefaultLogoutPageGeneratingFilter,
BasicAuthenticationFilter,
RequestCacheAwareFilter,
SecurityContextHolderAwareRequestFilter,
AnonymousAuthenticationFilter,
ExceptionTranslationFilter,
AuthorizationFilter
这是 Spring Security 默认的过滤器链。
2.1.4、访问测试
启动服务,浏览器访问测试 http://localhost:8080/res/echo
引入 Spring Security 以后,所有对于 Spring Web 接口的访问,都需要用户名密码。
用户名 user,密码在控制台,是一个 UUID。
输入用户名、密码登录之后,跳转回请求接口。
三、流程讲解
3.1、流程讲解
表单提交登录的认证过程如下图:
- 判断是否有权限访问资源
- 首先,用户向未获得授权的资源 /private(在我们例子中是 /res/echo)发出未经身份验证的请求。
- Spring Security 的 AuthorizationFilter(旧版本是 FilterSecurityInterceptor,Spring 官网图没有更新)通过抛出 AccessDeniedException 来指示未经身份验证的请求被拒绝。
- 由于用户未经过身份验证,因此 ExceptionTranslationFilter 启动 Start Authentication,并使用配置的 AuthenticationEntryPoint 将重定向发送到登录页面。在大多数情况下,AuthenticationEntryPoint 是 LoginUrlAuthenticationEntryPoint 的实例。
- 然后,浏览器将请求它重定向到的登录页面。
- DefaultLoginPageGeneratingFilter#generateLoginPageHtml 生成登录页内容返回给浏览器。
- 用户提交用户名密码后认证
- 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter#attemptAuthentication 方法通过从 HttpServletRequest 实例中提取用户名和密码来创建 UsernamePasswordAuthenticationToken(这是一种 Authentication 类型。Authentication 接口表示认证信息。可以是用户名密码、匿名访问,也可以是短信验证、二维码、指纹。具体取决于你自己的认证方式,由自己扩展)。
- 接下来,UsernamePasswordAuthenticationToken 被传递到 AuthenticationManager 实例ProviderManager#authenticate 中进行身份验证。AuthenticationManager 的处理细节取决于用户信息的存储方式(即 Authentication 的类型。比如,UsernamePasswordAuthenticationToken 由 ProviderManager 委托给DaoAuthenticationProvider 处理,AnonymousAuthenticationToken 委托给 AnonymousAuthenticationProvider 处理,用户也可以自己定义一个 QRCodeAuthenticationToken 表示二维码认证信息,再自己定义一个 QRCodeAuthenticationProvider 来处理)。
- 如果身份验证失败,则失败。
- SecurityContextHolder 被清除。
- 调用 RememberMeServices.loginFail。如果未配置 Remember me,则没有任何操作。
- AuthenticationFailureHandler 被调用。
4.如果身份验证成功,则成功。
- SessionAuthenticationStrategy 收到新登录的通知。
- Authentication 在 SecurityContextHolder上设置。
- 调用 RememberMeServices.loginSuccess。如果未配置 Remember me,则没有任何操作。
- ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent。
- 调用 AuthenticationSuccessHandler。通常,这是一个 SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它会重定向到 ExceptionTranslationFilter 保存的请求。
3.2、组件介绍
到此,我们先小结一下,介绍一下几个组件。
3.2.1、Authentication 认证信息接口
Authentication 对象在 Spring Security 中有两个主要目的:
1)封装用户身份验证的凭证
作为 AuthenticationManager 的输入,用于封装(用户提供的)用于身份验证的凭证。在这种情况下使用时,isAuthenticated()返回 false。可以是用户名密码,也可以是短信验证、二维码、指纹。具体取决于你自己的认证方式,由自己扩展。
2)表示当前经过身份验证的用户。
当前用户的 Authentication 可以从 SecurityContext 获得。
Authentication 对象包含:
- principal - 主体,用来标识用户。使用用户名/密码进行身份验证时,这通常是 UserDetails 的实例。
- credentials - 通常是密码。在许多情况下,这将在用户进行身份验证后被清除,以确保它不会被泄露。
- authorities - GrantedAuthorities 是授予用户的高级权限。
3.2.2、AuthenticationManager 认证管理器接口(Spring Security 扩展点)
@FunctionalInterface
public interface AuthenticationManager {
// 定义如何执行身份验证
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager 是定义如何执行身份验证的 API。由调用 AuthenticationManager 的控制器(即 Spring Security Filters 实例,比如 UsernamePasswordAuthenticationFilter)在SecurityContextHolder 上设置返回的 Authentication。
虽然 AuthenticationManager 的实现可以是任何内容,但最常见的实现是 ProviderManager。
- 怎么知道 AuthenticationManager 的默认实现是 ProviderManager?
3.2.3、ProviderManager
ProviderManager 是 AuthenticationManager 最常用的实现。ProviderManager 会将任务委托给一个 AuthenticationProvider 实例列表。每个 AuthenticationProvider 都有机会表明认证应该成功、失败,或者表明它无法做出决定,并允许下游的 AuthenticationProvider 来做决定。如果配置的 AuthenticationProvider 实例都无法进行认证,认证将以 ProviderNotFoundException 失败,这是一个特殊的 AuthenticationException,表明 ProviderManager 未配置支持传入的 Authentication 类型的 AuthenticationProvider。
这句话的意思是,ProviderManager 中存在一个 AuthenticationProvider 数组。请求被 ProviderManager 委托给 AuthenticationProvider 数组进行真正的认证处理。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private List<AuthenticationProvider> providers = Collections.emptyList();
}
ProviderManager 中调用 AuthenticationProvider 伪代码如下:
public Authentication authenticate(Authentication authentication) {
Class<? extends Authentication> toTest = authentication.getClass();
Authentication result = null;
Authentication parentResult = null;
for (AuthenticationProvider provider : this.getProviders()) {
// 遍历 provider 数组
// 判断当前 provider 是否有能力处理给定 token 类型的认证
// 不行就交给下游 provider 处理
if (!provider.supports(toTest)) {
continue;
}
// 调用 provider#authenticate 接口进行身份认证
result = provider.authenticate(authentication);
}
if (result == null && this.parent != null) {
// 如果当前 AuthenticationManager 都无法处理此种类型的 token
// 则委托给它的 parent 处理
// parent 其实也是委托给它持有的 providers 数组处理
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
// 如果最后连 parent 都处理不了
// 抛出 AuthenticationException 异常
}
}
3.2.4、AuthenticationProvider 认证服务提供者接口(Spring Security 扩展点)
AuthenticationProvider 是真正执行认证逻辑的接口。
你可以将多个 AuthenticationProvider 实例注入到 ProviderManager 中。每个 AuthenticationProvider 执行一种特定类型的认证。例如,DaoAuthenticationProvider 支持基于用户名/密码的认证,处理 UsernamePasswordAuthenticationToken。AnonymousAuthenticationProvider 支持不需要认证的匿名访问,处理 AnonymousAuthenticationToken。
每一个 AuthenticationProvider 都定义了两个接口,一个是认证处理逻辑;一个是声明它支持的 token 类型。用户也可以自己定义一个 QRCodeAuthenticationToken 表示二维码认证信息,再自己定义一个 QRCodeAuthenticationProvider supports 这个 Token 来处理。
public interface AuthenticationProvider {
// 定义认证逻辑
Authentication authenticate(Authentication authentication) throws AuthenticationException;
// 声明支持的 token 类型
boolean supports(Class<?> authentication);
}
-----------------------------------------
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
-----------------------------------------
public class AnonymousAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
@Override
public boolean supports(Class<?> authentication) {
return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
}
}
3.2.5、双亲委派
如果你将断点打在 AuthenticationManagerBuilder#performBuild 方法中,会发现它跑两次。
AuthenticationManagerBuilder 是用来构造 AuthenticationManager 的构造器,它跑了两次,是因为它构造了两个 ProviderManager,形成父子关系。
ProviderManager 还允许配置一个可选的父级 AuthenticationManager,如果没有任何 AuthenticationProvider 能够执行认证,就会委派给该父级管理器(正如前面伪代码分析的那样)。父级可以是任何类型的 AuthenticationManager,但通常是一个 ProviderManager 实例 。
如果不自己配置,Spring Security 默认会给我们构造出如下图的一个 ProviderManager 父子关系:
之所以用双亲委派,其实就是定义一种兜底的方案。用户名/密码的认证是一种最常见的认证方式,把它放在父 ProviderManager 中,可以作为共享的认证方式。
实际上,多个 ProviderManager 实例可能会共享同一个父级 AuthenticationManager。在存在多个 SecurityFilterChain 实例且这些实例具有一些共同的认证(共享的父级 AuthenticationManager),但也有不同的认证机制(不同的 ProviderManager 实例)的情况下,这种情况有点常见 。
3.2.6、DaoAuthenticationProvider 用户名/密码认证服务提供者实现
DaoAuthenticationProvider 是一个 AuthenticationProvider 实现,它使用 UserDetailsService 和 PasswordEncoder 对用户名和密码进行身份验证。
- 身份验证过滤器 UsernamePasswordAuthenticationFilter 将 UsernamePasswordAuthenticationToken 传递给 AuthenticationManager,AuthenticationManager 是由 ProviderManager 实现的。
- ProviderManager 配置为使用 DaoAuthenticationProvider 类型的 AuthenticationProvider。
- DaoAuthenticationProvider 从 UserDetailsService 中查找 UserDetails。
- DaoAuthenticationProvider 使用 PasswordEncoder 对上一 步返回的 UserDetails 中的密码进行验证。
- 身份验证成功后,返回的 Authentication 为 UsernamePasswordAuthenticationToken 类型,并且具有一个主体 principal,该 principal 是 UserDetailsService 返回的 UserDetails。最终,返回的 UsernamePasswordAuthenticationToken 由身份验证过滤器 UsernamePasswordAuthenticationFilter 在 SecurityContextHolder 上设置。
- DaoAuthenticationProvider 怎么加入到 ProviderManager 的 providers 数组的?
- DaoAuthenticationProvider 持有 UserDetailsService 和 PasswordEncoder
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private PasswordEncoder passwordEncoder; private UserDetailsService userDetailsService; }
- InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer
class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationConfigurerAdapter { // 容器对象 private final ApplicationContext context; class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter { public void configure(AuthenticationManagerBuilder auth) throws Exception { // 从容器中获取 UserDetailsService 和 PasswordEncoder UserDetailsService userDetailsService = (UserDetailsService)InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0],UserDetailsService.class); PasswordEncoder passwordEncoder = (PasswordEncoder)this.getBeanOrNull(PasswordEncoder.class); DaoAuthenticationProvider provider; if (passwordEncoder != null) { // 如果你有提供了,走这里 provider = new DaoAuthenticationProvider(passwordEncoder); } else { // 如果没有提供,走这里 provider = new DaoAuthenticationProvider(); } provider.setUserDetailsService(userDetailsService); } } private <T> T getBeanOrNull(Class<T> type) { return InitializeUserDetailsBeanManagerConfigurer.this.context .getBeanProvider(type).getIfUnique(); } }
PasswordEncoder 和 UserDetailsService 都是从 IOC 容器中获取的,这为后续扩展提供了一种可能。Spring Security 它可以配置一个默认的实现,但是这个默认的实现以条件注解的方式注入 ioc 容器,如果我们提供了自定义实现,那么默认的实现就不注入。
3.2.7、UserDetailsService 用户详情加载服务接口(Spring Security 扩展点)
public interface UserDetailsService {
// 它里面只有一个 根据用户名加载 UserDetails 的接口
// UserDetails 可以把它理解为 表示用户信息的 接口
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService 由 DaoAuthenticationProvider 用于检索用户名、密码和其他属性,以便使用用户名和密码进行身份验证。Spring Security 提供了 UserDetailsService 的内存(InMemoryUserDetailsManager)、JDBC(JdbcUserDetailsManager)和缓存(CachingUserDetailsService)实现。JDBC 的实现,用户的所有表结构,都要按照它的来,实际很少使用。
- 怎么确定 UserDetailsService 默认实现是 InMemoryUserDetailsManager?
在 Spring Boot 自动装配导入的类中,有一个 UserDetailsServiceAutoConfiguration
它通过条件注解的方式注入了 InMemoryUserDetailsManager
@ConditionalOnMissingBean( // 这四个都是 Spring Security 的扩展点 // 如果你自己定义了,那就用你的 value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class} )
只有当 AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class 类型的 bean 都不在 Spring应用上下文中存在时,UserDetailsServiceAutoConfiguration 才会被创建。如果其中任意一个类型已经有一个 bean 存在于上下文中,则 UserDetailsServiceAutoConfiguration 将不会被创建。
- InMemoryUserDetailsManager 初始化用的是配置是 SecurityProperties
// 从配置文件获取 @ConfigurationProperties(prefix = "spring.security") public class SecurityProperties { public static class User { private String name = "user"; private String password = UUID.randomUUID().toString(); private List<String> roles = new ArrayList(); private boolean passwordGenerated = true; } }
这也解释了,为什么 Spring Security 默认用户名是 user,而它的密码是 UUID.
3.2.8、PasswordEncoder 密码编码接口(Spring Security 扩展点)
public interface PasswordEncoder {
// 密码加密
String encode(CharSequence rawPassword);
// 密码匹配
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
Spring Security 的 PasswordEncoder 接口用于执行密码的单向转换,以使密码被安全地存储。通常,PasswordEncoder 用于存储需要在身份验证时与用户提供的密码进行比较的密码。
- 怎么确定 PasswordEncoder 默认实例是 NoOpPasswordEncoder?
还记得 DaoAuthenticationProvider 么?在InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer 初始化 DaoAuthenticationProvider 时
class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationConfigurerAdapter { // 容器对象 private final ApplicationContext context; class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter { public void configure(AuthenticationManagerBuilder auth) throws Exception { UserDetailsService userDetailsService = (UserDetailsService)InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0],UserDetailsService.class); // 从 IOC 容器中查找 PasswordEncoder passwordEncoder = (PasswordEncoder)this.getBeanOrNull(PasswordEncoder.class); DaoAuthenticationProvider provider; if (passwordEncoder != null) { // 如果你有提供了,走这里 provider = new DaoAuthenticationProvider(passwordEncoder); } else { // 如果没有提供,走这里 provider = new DaoAuthenticationProvider(); } provider.setUserDetailsService(userDetailsService); } } private <T> T getBeanOrNull(Class<T> type) { return InitializeUserDetailsBeanManagerConfigurer.this.context .getBeanProvider(type).getIfUnique(); } }
如果你在配置中提供了 PasswordEncoder,Spring Security 会用你提供的 PasswordEncoder,如果没有,它调用无参的构造函数来构造 DaoAuthenticationProvider。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { public DaoAuthenticationProvider() { this(PasswordEncoderFactories.createDelegatingPasswordEncoder()); } }
无参构造函数调用密码编码工厂 PasswordEncoderFactories,生成一个 DelegatingPasswordEncoder 它类似一个 Map 结构:
public final class PasswordEncoderFactories { public static PasswordEncoder createDelegatingPasswordEncoder() { String encodingId = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap(); encoders.put(encodingId, new BCryptPasswordEncoder()); encoders.put("ldap", new LdapShaPasswordEncoder()); encoders.put("MD4", new Md4PasswordEncoder()); encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()); encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()); encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new StandardPasswordEncoder()); encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()); encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); return new DelegatingPasswordEncoder(encodingId, encoders); } }
然后,还记得 InMemoryUserDetailsManager 吗?
在 Spring Boot 自动装配导入的类中,有一个 UserDetailsServiceAutoConfiguration
它通过条件注解的方式注入了 InMemoryUserDetailsManager
这里在生成 InMemoryUserDetailsManager 实例的时候,给默认用户 user 密码做了一次加工,当没有 PasswordEncoder 的时候,在密码前面加上 {noop},最后生成的密码类似
{noop}c0d16efe-9468-487a-8132-5bd3988ff030
最后,在 DaoAuthenticationProvider 进行身份认证的时候
调用 DelegatingPasswordEncoder matches
从 prefixEncodedPassword {noop}c0d16efe-9468-487a-8132-5bd3988ff030 解析出 noop,
然后用 noop 从 DelegatingPasswordEncoder 的 map 中拿到 NoOpPasswordEncoder,
去掉 prefixEncodedPassword 的前缀 {noop},与用户传进来的密码 rawPassword 进行匹配。
NoOpPasswordEncoder 的匹配逻辑是,不做加工,原样匹配。它还是一个单例类。
public final class NoOpPasswordEncoder implements PasswordEncoder { private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder(); private NoOpPasswordEncoder() { } public String encode(CharSequence rawPassword) { return rawPassword.toString(); } public boolean matches(CharSequence rawPassword, String encodedPassword) { // 不做加工,原样匹配 return rawPassword.toString().equals(encodedPassword); } public static PasswordEncoder getInstance() { return INSTANCE; } }
四、自定义用户名密码
4.1、使用 application.yml
spring:
application:
name: hello-security
security:
user:
name: admin
password: 123456
roles:
- admin
重启服务,使用 admin/123456 也能登录成功。
4.2、使用 Java Bean 配置方式
- 方式一:User.UserBuilder
@Configuration
@EnableWebSecurity // 开启 spring security
public class SecurityConfig {
@Bean
public UserDetailsService users() {
// 使用默认加密方式 bcrypt 对密码进行加密,添加用户信息
User.UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("123456")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("123456")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
- 方式二:{id}encodedPassword
Spring Security 密码加密格式为:{id}encodedPassword
@Configuration
@EnableWebSecurity // 开启 spring security
public class SecurityConfig {
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$9ItJHL.xW70xCcX79dj4lObvRXK9dOyRe1xJPFhanapE5hbwqeYl2")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$Q.28EEhF2WEvc4d4311Xj.WyeGlm9y3AMz8Fh60rjNMZOAFZ0br9y")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
方式三:自定义 PasswordEncoder
@Configuration
@EnableWebSecurity // 开启 spring security
public class SecurityConfig {
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("123456"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("123456"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
重启服务,这三种方式用 user/123456、admin/123456 都可以登录。
- 为什么 Spring Security 使用 BCryptPasswordEncoder 做为默认加密方式?
1、单向加密性
public class Test { public static void main(String[] args) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String aa = encoder.encode("123456"); String bb = encoder.encode("123456"); System.out.println(aa); System.out.println(bb); } } // 对于同一个密码,每次加密输出的都不相同 $2a$10$9ItJHL.xW70xCcX79dj4lObvRXK9dOyRe1xJPFhanapE5hbwqeYl2 $2a$10$Q.28EEhF2WEvc4d4311Xj.WyeGlm9y3AMz8Fh60rjNMZOAFZ0br9y
五、自定义用户信息加载方式
实现 UserDetailsService 接口,自定义从数据库获取用户信息。
- SecurityConfig
@Configuration
@EnableWebSecurity // 开启 spring security
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- DbUserDetailsService
@Service
public class DbUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 用 map 模拟从数据库获取用户信息,并封装成 UserDetails 对象
Map<String, User> map = new HashMap<>();
User user1 = new User();
user1.setUsername("user");
user1.setPassword(passwordEncoder.encode("123456")); // 数据库里面要存密文
user1.setRoles("USER");
map.put("user", user1);
User user2 = new User();
user2.setUsername("admin");
user2.setPassword(passwordEncoder.encode("123456"));
user2.setRoles("USER", "ADMIN");
map.put("admin", user2);
// 查询
User user = map.get(username);
// 封装成 UserDetails 对象返回
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles())
.build();
}
public static class User {
private String username;
private String password;
private final List<String> roles = new ArrayList<>();
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String[] getRoles() {
return roles.toArray(String[]::new);
}
public void setRoles(String... roles) {
this.roles.addAll(Arrays.asList(roles));
}
}
}
六、自定义登录页面
在讲解自定义登录页面之前,有必要讲解两个比较重要的组件:SecurityFilterChain 和 HttpSecurity。
6.1、组件介绍
6.1.1、SecurityFilterChain Security 过滤器链接口(Spring Security 扩展点)
我们开头讲过 Spring Security 本质是一个过滤器链。这个过滤器链就是 SecurityFilterChain。
SecurityFilterChain 有一个 Filter 列表:
public interface SecurityFilterChain {
// 定义请求是否需要交由该过滤器链处理
boolean matches(HttpServletRequest request);
// 获取过滤器列表
List<Filter> getFilters();
}
------------------------------------------------
// DefaultSecurityFilterChain 是接口 SecurityFilterChain 唯一默认实现
public final class DefaultSecurityFilterChain implements SecurityFilterChain, BeanNameAware, BeanFactoryAware {
// 持有过滤器列表
private final List<Filter> filters;
// ...... 其他属性
}
它里面的过滤器,就是类似于我们之前打印出来的:
2025-04-09 17:15:57 - Will secure any request with filters:
DisableEncodeUrlFilter,
WebAsyncManagerIntegrationFilter,
SecurityContextHolderFilter,
HeaderWriterFilter,
CsrfFilter,
LogoutFilter,
UsernamePasswordAuthenticationFilter,
DefaultResourcesFilter,
DefaultLoginPageGeneratingFilter,
DefaultLogoutPageGeneratingFilter,
BasicAuthenticationFilter,
RequestCacheAwareFilter,
SecurityContextHolderAwareRequestFilter,
AnonymousAuthenticationFilter,
ExceptionTranslationFilter,
AuthorizationFilter
可是我们之前并没有自己定义 SecurityFilterChain 或者这些过滤器,那它们是怎么来的呢?
- 默认 SecurityFilterChain 初始化
在 Spring Boot 自动装配导入的类中,有一个 SecurityAutoConfiguration
SecurityAutoConfiguration 导入了 SpringBootWebSecurityConfiguration
在 SpringBootWebSecurityConfiguration 初始化的时候,会往容器中生成一个 SecurityFilterChain 的实例 DefaultSecurityFilterChain。
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) class SpringBootWebSecurityConfiguration { @Configuration(proxyBeanMethods = false) // 注意这个条件注解 @ConditionalOnDefaultWebSecurity static class SecurityFilterChainConfiguration { @Bean @Order(SecurityProperties.BASIC_AUTH_ORDER) SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); http.formLogin(withDefaults()); http.httpBasic(withDefaults()); // 生成 DefaultSecurityFilterChain return http.build(); } } }
请注意 @ConditionalOnDefaultWebSecurity 这个条件注解:
@ConditionalOnDefaultWebSecurity 这个条件注解依赖于 DefaultWebSecurityCondition 这个条件类。
DefaultWebSecurityCondition 条件类包含两个静态内部类:
- Classes: 这个内部类使用了 @ConditionalOnClass 注解,表示只有当 SecurityFilterChain.class 和 HttpSecurity.class 类存在于类路径上时,才会满足这个条件。
- Beans: 这个内部类使用了 @ConditionalOnMissingBean 注解,表示只有当 Spring 容器中不存在 SecurityFilterChain.class 类型的 Bean 时,才会满足这个条件。
这两个条件是通过 AllNestedConditions 组合在一起的,默认情况下需要同时满足所有嵌套的条件才能使整个 DefaultWebSecurityCondition 满足。这意味着,只有当 SecurityFilterChain 和 HttpSecurity 类存在,并且 Spring 容器中没有 SecurityFilterChain 类型的 Bean 对象时, @ConditionalOnDefaultWebSecurity 被这个注解的配置才会被启用。SecurityFilterChain defaultSecurityFilterChain 才会被注入到 IOC 容器中。这是 Spring Security 的一个扩展点。
如果我们项目中配置了 SecurityFilterChain,则 Spring Security 默认的 SecurityFilterChain 配置就不会生效。
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(DefaultWebSecurityCondition.class) public @interface ConditionalOnDefaultWebSecurity { } ---------------------------------------------------------------- // DefaultWebSecurityCondition class DefaultWebSecurityCondition extends AllNestedConditions { DefaultWebSecurityCondition() { super(ConfigurationPhase.REGISTER_BEAN); } @ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class }) static class Classes { } @ConditionalOnMissingBean({ SecurityFilterChain.class }) static class Beans { } }
- 默认的 Security Filters 是怎么加入到 SecurityFilterChain 的列表 filters 中的?
首先,HttpSecurityConfiguration 生成 HttpSecurity 实例的时候,往 HttpSecurity 的 LinkedHashMap configurers(该属性继承自 AbstractConfiguredSecurityBuilder) 加入13个 Configurer。这些 Configurer 是对应 Filter 的装配器。
@Configuration(proxyBeanMethods = false) class HttpSecurityConfiguration { @Bean(HTTPSECURITY_BEAN_NAME) @Scope("prototype") HttpSecurity httpSecurity() throws Exception { LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context); AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder( this.objectPostProcessor, passwordEncoder); authenticationBuilder.parentAuthenticationManager(authenticationManager()); authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher()); HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects()); WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter(); webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); http // 加入 CsrfConfigurer .csrf(withDefaults()) .addFilter(webAsyncManagerIntegrationFilter) // 加入 ExceptionHandlingConfigurer .exceptionHandling(withDefaults()) // 加入 HeadersConfigurer .headers(withDefaults()) // 加入 SessionManagementConfigurer .sessionManagement(withDefaults()) // 加入 SecurityContextConfigurer .securityContext(withDefaults()) // 加入 RequestCacheConfigurer .requestCache(withDefaults()) // 加入 AnonymousConfigurer .anonymous(withDefaults()) // 加入 ServletApiConfigurer .servletApi(withDefaults()) // 加入 DefaultLoginPageConfigurer .apply(new DefaultLoginPageConfigurer<>()); // 加入 LogoutConfigurer http.logout(withDefaults()); // 不加入 CorsConfigurer applyCorsIfAvailable(http); // 不加入 AbstractHttpConfigurer applyDefaultConfigurers(http); return http; } }
然后在 Spring Boot 自动装配导入的类中,有一个 SecurityAutoConfiguration
SecurityAutoConfiguration 导入了 SpringBootWebSecurityConfiguration
在 SpringBootWebSecurityConfiguration 生成 SecurityFilterChain 实例的时候,会调用 HttpSecurity 的 build 方法,挨个调用 Configurer 生成 13 个 Filter,加入到 HttpSecurity 的 List<OrderedFilter> filters 里面。
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) class SpringBootWebSecurityConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnDefaultWebSecurity static class SecurityFilterChainConfiguration { @Bean @Order(SecurityProperties.BASIC_AUTH_ORDER) SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { // 加入 AuthorizeHttpRequestsConfigurer http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); // 加入 FormLoginConfigurer http.formLogin(withDefaults()); // 加入 HttpBasicConfigurer http.httpBasic(withDefaults()); // 到此,13个 Configurer 加入结束 return http.build(); } } }
这里单看一个 CsrfConfigurer,其他类似
最后,在生成 SecurityFilterChain 的实现 DefaultSecurityFilterChain 的时候,把 HttpSecurity 持有的 filters 包装一下,传递给 DefaultSecurityFilterChain。
HttpSecurity.filters --> DefaultSecurityFilterChain.filters
从这可以看出,HttpSecurity 是 SecurityFilterChain 的装配流水线。我们可以给 HttpSecurity 配置各种过滤器 Configurer,来定制我们过滤器链。HttpSecurity 只有在 http.build(); 的时候,才真正生成并配置 SecurityFilterChain。
6.1.2、Configurer 与 Filter 对应关系
Configurer | Filter |
FormLoginConfigurer | UsernamePasswordAuthenticationFilter |
CsrfConfigurer | CsrfFilter |
LogoutConfigurer | LogoutFilter |
HttpBasicConfigurer | BasicAuthenticationFilter |
RequestCacheConfigurer | RequestCacheAwareFilter |
AnonymousConfigurer | AnonymousAuthenticationFilter |
ExceptionHandlingConfigurer | ExceptionTranslationFilter |
HeadersConfigurer | HeaderWriterFilter |
没有 Configurer | WebAsyncManagerIntegrationFilter |
SessionManagementConfigurer | DisableEncodeUrlFilter |
SecurityContextConfigurer | SecurityContextHolderFilter |
DefaultLoginPageConfigurer | DefaultLoginPageGeneratingFilter |
DefaultLogoutPageGeneratingFilter | |
DefaultResourcesFilter | |
ServletApiConfigurer | SecurityContextHolderAwareRequestFilter |
AuthorizeHttpRequestsConfigurer | AuthorizationFilter |
未完待续。。。。。。。