在学习定制 Spring Security的过程中,阅读了 Spring Security 的官方文档、《Spring Security in action》和《OAuth 2 in action》后,并结合源码摸清了 Spring Security 的工作流程,把这些知识梳理成了图片和文字,花了我足足一个月,这没想到 Spring Security 也有这么多内容。
🌊0 概览
0.1 流程分析
0.1.1 UserPasswordAuthentication 流程
假设要访问的接口为 /private,登录接口为 /login,登录页面为 login.html

- 首先,用户向其未授权的资源 /private 发出未经身份验证的请求
- 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter 通过从 HttpServletRequest 中提取用户名和密码来创建一个 UsernamePasswordAuthenticationToken
- 接下来,将 UsernamePasswordAuthenticationToken 传入 AuthenticationManager 进行身份验证
- 给 ProviderManager 配置 AuthenticationProvider 的子类 DaoAuthenticationProvider
- DaoAuthenticationProvider 通过 UserDetailsService 的子类 JDBCUserDetailManager 中查找 UserDetails
- JDBCUserDetailManager 在数据库中查找用户并返回其用户信息
- 如果找到用户,PasswordEncoder 会比对用户请求发来的用户信息和从数据库读取的用户信息,若验证成功到第 8 步,验证失败到第 10 步
- 用户信息会被放进 UsernamePasswordAuthenticationToken 并返回,继续到第 9 步
- 将 UsernamePasswordAuthenticationToken 放进 SecurityContextHolder
- 通过抛出 AccessDeniedException 指示未经身份验证的请求被拒绝,继续到第 11 步
- 使用配置的 AuthenticationEntryPoint 将重定向发送到登录页面 Location:/login,继续到第 12 步
- 浏览器将请求它被重定向到的登录页面 GET /login,继续到第 13 步
- 返回登录页面 login.html
0.1.2 Basic HTTP Authentication 流程

- 首先,用户向其未授权的资源 /private 发出未经身份验证的请求
- 当用户提交他们的用户名和密码时,BasicAuthenticationFilter 通过从 HttpServletRequest 中提取用户名和密码来创建一个 UsernamePasswordAuthenticationToken
- 接下来,将 UsernamePasswordAuthenticationToken 传入 AuthenticationManager 进行身份验证,身份验证过程和 UserPasswordAuthentication 的类似。若验证成功则到第 4 步,若验证失败则到第 6 步
- 将 Authentication 存到 SecurityContextHolder,继续到第 5 步
- 调用 RememberMeServices.loginSuccess ,如果 remember me 没有配置,则是一个空操作;BasicAuthenticationFilter 调用 FilterChain.doFilter(request,response) 以继续其余的应用程序逻辑
- 清空 SecurityContextHolder;调用 RememberMeServices.loginFail,如果 remember me 没有配置,则是一个空操作;FilterSecurityInterceptor 通过抛出 AccessDeniedException 指示未经身份验证的请求被拒绝,继续到第 7 步
- AuthenticationEntryPoint 被调用以触发 WWW-Authenticate 被再次发送
0.1.3 授权流程

- FilterSecurityInterceptor 从 SecurityContextHolder 获取 Authentication
- FilterSecurityInterceptor 将接收到的 HttpServletRequest、HttpServletResponse 和 FilterChain 创建一个 FilterInvocation
- FilterSecurityInterceptor 将 FilterInvocation 传递给 SecurityMetadataSource 以获取多个 ConfigAttribute
- FilterSecurityInterceptor 将 Authentication、FilterInvocation 和 ConfigAttributes 传递给 AccessDecisionManager,如果访问已经授权,FilterSecurityInterceptor 继续执行 FilterChain,否则继续到第 5 步
- 抛出 AccessDeniedException
0.1.4 OAuth2 流程
Resource Server (使用 JWT)

- 用户向其未授权的资源 /private 发出未经身份验证的请求
- 当用户提交 bearer token 时,BearerTokenAuthenticationFilter 通过从 HttpServletRequest 中提取令牌来创建一个 BearerTokenAuthenticationToken
- HttpServletRequest 传递给 AuthenticationManagerResolver,后者选择 AuthenticationManager,BearerTokenAuthenticationToken 传入 AuthenticationManager 进行认证
- ProviderManager 被配置去使用 AuthenticationProvider 的子类 JwtAuthenticationProvider
- JwtAuthenticationProvider 使用 JwtDecoder 解码、校验 Jwt
- JwtAuthenticationProvider 使用 JwtAuthenticationConverter 将 Jwt 转换为 GrantedAuthority 的集合,若验证成功则到第 7 步,若验证失败则到第 9 步
- 返回的 Authentication 是 JwtAuthenticationToken,并且有一个主体,它是由配置的 JwtDecoder 返回的 Jwt,继续到第 8 步
- 返回的 JwtAuthenticationToken 将被放到 SecurityContextHolder,BearerTokenAuthenticationFilter 调用 FilterChain.doFilter(request,response) 以继续其余的应用程序逻辑
- FilterSecurityInterceptor 通过抛出 AccessDeniedException 表示未经身份验证的请求被拒绝,继续到第 10 步
- SecurityContextHolder 被清空,调用 AuthenticationEntryPoint 触发 WWW-Authenticate 头再次发送
Client

重定向
- resource owner 在浏览器中发出请求 GET / oauth2 / authorization / {registrationId}
- OAuth2AuthorizationRequestRedirectFilter 将 registrationId 作为参数传入并调用 ClientRegistrationRepository 接口的findByRegistrationId() 方法,findByRegistrationId() 返回 ClientRegistration
- OAuth2AuthorizationRequestRedirectFilter 根据 ClientRegistration 生成 OAuth2AuthorizationRequest,并调用 AuthorizationRequestRepository 的 saveAuthorizationRequest() 方法跨会话共享 OAuth2AuthorizationRequest
- OAuth2AuthorizationRequestRedirectFilter 从 ClientRegistration 生成一个 URL 发送到 Authorization Server 的 Authorization 端点,Authorization Server 返回登录页面
预认证处理
- resource owner 在登陆页面提交用户信息并发送登录请求
- OAuth2LoginAuthenticationFilter 从请求中分析来自 Authorization Server 的授权响应,并生成 OAuth2AuthorizationResponse
- OAuth2LoginAuthenticationFilter 调用 AuthorizationRequestRepository 接口的 loadAuthorizationRequest() 方法来获得 OAuth2AuthorizationRequest
- OAuth2LoginAuthenticationFilter 将 registrationId 作为参数传入并调用 ClientRegistrationRepository 接口的findByRegistrationId() 方法,findByRegistrationId() 返回 ClientRegistration
认证流程
- OAuth2LoginAuthenticationFilter 生成包含 OAuth2AuthorizationRequest、OAuth2AuthorizationResponse 和 ClientRegistration 的 OAuth2LoginAuthenticationToken
- OAuth2LoginAuthenticationProvider 通过调用 OAuth2AccessTokenResponseClient 接口的 getTokenResponse() 方法从 Authorization Server 的 Token 端点获取 Access Token
- OAuth2LoginAuthenticationProvider 通过调用 OAuth2UserService 接口的 loadUser() 方法从 Authorization Server 的 Authorization 端点获取用户信息
- OAuth2LoginAuthenticationProvider 生成 OAuth2LoginAuthenticationToken 返回认证结果
认证后处理
- OAuth2LoginAuthenticationFilter 根据认证结果生成 OAuth2AnthenticationToken 并设置在SecurityContext中
- OAuth2LoginAuthenticationFilter 根据认证结果生成 OAuth2AuthorizedClient ,调用OAuth2AuthorizedClientService 的 saveAuthorizedClinet() 方法,将 OAuth2AuthorizedClient 保存在任意类可访问的区域
0.2 Spring Security 自带 filter 执行顺序
绿框内的为本文涉及的过滤器

0.3 文章结构

0.6 主要依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
🌊1 管理用户
1.1 使用 UserDetails 描述用户
1.1.1 UserDetails 的定义
public interface UserDetails extends Serializable {
// 返回用户凭证
String getUsername();
String getPassword();
// 返回用户权限列表
Collection<? extends GrantedAuthority> getAuthorities();
// 管理用户状态
// 如果不需要实现以下功能,可以让这些方法都返回 true
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
1.1.2 GrantedAuthority 的定义
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
1.1.3 GrantedAuthority 的实例化
// SimpleGrantedAuthority 是 GrantedAuthority 的一个基础实现,以字符串形式描述权限
GrantedAuthority g2 = new SimpleGrantedAuthority("READ");
// 或者使用 lambda 表达式
GrantedAuthority g1 = () -> "READ";
1.1.4 实现 UserDetails
根据单一职责原则,一个类只有一个职责,所以当用户类仅作为验证,则为 ”单原则“,若用户类不仅用于验证,同时还代表数据库中的一个实体类则为多职责
单职责
-
继承 Userdetails
public class DummyUser implements UserDetails { @Override public String getUsername() { return "bill"; } @Override public String getPassword() { return "12345"; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(() -> "READ"); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } -
使用 User 类的静态方法 不需要自定义 Userdetails 的话就直接使用 User 类的静态方法
-
例子
// 至少要提供 username 和 password,且 username 不能为空串 // 此处 User.withUsername("bill") 返回的是 User.UserBuilder 的实例(见下方“原理”) UserDetails u = User.withUsername("bill") .password("12345") .authorities("read", "write") .accountExpired(false) .disabled(true) .build(); -
原理
User.UserBuilder builder1 = User.withUsername("bill"); UserDetails u1 = builder1 .password("12345") .authorities("read", "write") .passwordEncoder(p -> encode(p)) .accountExpired(false) .disabled(true) .build();
-
多职责
-
继承 Userdetails 并使用装饰器模式
-
数据库实体类
public class MyUser { private Long id; private String username; private String password; private String authority; // 忽略 getters and setters } -
创建有两个职责的用户类
public class SecurityUser implements UserDetails { private final MyUser user; public SecurityUser(MyUser user) { this.user = user; } @Override public String getUsername() { return user.getUsername(); } @Override public String getPassword() { return user.getPassword(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(() -> user.getAuthority()); } // 忽略代码 }
-
1.2 使用 JDBCUserdetailsManager 管理用户
1.2.1 UserDetailsService 的定义
public interface UserDetailsService {
// UsernameNotFoundException 是运行时异常,继承自 AuthenticationException(所有验证过程异常的父类)
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
1.2.2 UserDetailsManager 的定义
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
1.2.3 在配置类中 JDBCUserdetailsManager
-
@Configuration public class ProjectConfig { @Bean public UserDetailsService userDetailsService(DataSource dataSource) { return new JdbcUserDetailsManager(dataSource); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } } -
若想修改默认的数据库查询语句
@Bean public UserDetailsService userDetailsService(DataSource dataSource) { String usersByUsernameQuery = "select username, password, enabled from users where username = ?"; String authsByUserQuery = "select username, authority from spring.authorities where username = ?"; var userDetailsManager = new JdbcUserDetailsManager(dataSource); userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery); userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery); return userDetailsManager; }
🌊2 处理密码
2.1 实现 PasswordEncoder
2.1.1 PasswordEncoder 的定义
public interface PasswordEncoder {
// 返回编码结果
String encode(CharSequence rawPassword);
// 比对密码
boolean matches(CharSequence rawPassword, String encodedPassword);
// 默认为 false,如果重写改成返回 true,已经编码过的密码会被再一次编码,以达到更安全的目的
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
2.1.2 使用 SHA-512 实现 PasswordEncoder
public class Sha512PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return hashWithSHA512(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String hashedPassword = encode(rawPassword);
return encodedPassword.equals(hashedPassword);
}
private String hashWithSHA512(String input) {
StringBuilder result = new StringBuilder();
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte [] digested = md.digest(input.getBytes());
for (int i = 0; i < digested.length; i++) {
result.append(Integer.toHexString(0xFF & digested[i]));
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Bad algorithm");
}
return result.toString();
}
}
2.2 使用 PasswordEncoder 子类
2.2.1 Pbkdf2PasswordEncoder
使用 PBKDF2 算法
PasswordEncoder p = new Pbkdf2PasswordEncoder();
// 参数:用于加密的密钥
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret");
// 第一个参数:用于加密的密钥
// 第二个参数:给密码编码的迭代次数,默认值为 185000
// 第三个参数:哈希的长度,默认值为 256
// 后面两个参数影响编码结果的强度
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret", 185000, 256);
2.2.2 BCryptPasswordEncoder
使用 bcrypt 算法
PasswordEncoder p = new BCryptPasswordEncoder();
// 参数会影响哈希操作的迭代次数,若 参数 = n,则 迭代次数 = 2 ^ n (n >= 4 && n <= 31)
PasswordEncoder p = new BCryptPasswordEncoder(4);
2.2.3 SCryptPasswordEncoder
使用 scrypt 算法
PasswordEncoder p = new SCryptPasswordEncoder();
PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);

2.2.4 DelegatingPasswordEncoder
将 <加密算法名>-实例存储到键值对中
创建实例
@Configuration
public class ProjectConfig {
// Omitted code
@Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
// 第一个参数:默认的加密算法
/*
基于哈希的前缀,DelegatingPassword-Encoder 使用相应的 PasswordEncoder 实现来匹配密码,
例如 {bcrypt}$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG,
前缀为 {bcrypt} 所以使用 BCryptPasswordEncoder
*/
return new DelegatingPasswordEncoder("bcrypt", encoders);
}
}
使用 PasswordEncoderFactories
// DelegatingPasswordEncoder 的实现,默认使用 bcrypt 算法编码
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
复制代码
2.2.5 Spring Security Crypto module (SSCM)
Spring Security Crypto 模块提供对对称加密、密钥生成和密码编码的支持
KeyGenerators
KeyGenerators 类提供了许多方便的工厂方法来构造不同类型的密钥生成器
-
StringKeyGenerator 生成字符串形式的密钥
-
定义
-
public interface StringKeyGenerator { // 创建一个 8 字节的密钥,并将其编码为十六进制字符串 String generateKey(); }
-
-
实例化
-
StringKeyGenerator keyGenerator = KeyGenerators.string(); String salt = keyGenerator.generateKey();
-
-
-
BytesKeyGenerator 生成字节[] 形式的密钥
-
定义
-
public interface BytesKeyGenerator { // 以字节数返回密钥长度的方法 // 默认生成 8 字节长度的密钥 int getKeyLength(); byte[] generateKey(); }
-
-
实例化 可以使用 KeyGenerators.secureRandom() 和 KeyGenerators.shared(int length) 生成 BytesKeyGenerator 实例
-
KeyGenerators.shared(int length) 生成的 BytesKeyGenerator 的实例在输入不变时,每次调用 generateKey() 都产生相同的结果
-
BytesKeyGenerator keyGenerator = KeyGenerators.shared(16); byte [] key = keyGenerator.generateKey(); int keyLength = keyGenerator.getKeyLength();
-
-
KeyGenerators.secureRandom() 生成的 BytesKeyGenerator 的实例在输入不变时,每次调用 generateKey() 都产生不同的
-
-

本文详述了Spring Security的用户验证、HTTP基本认证、授权流程以及OAuth2的集成,涵盖UserDetails、PasswordEncoder、AuthenticationProvider、Filter的实现,同时探讨了JWT和加密签名的使用。内容包括Spring Security的工作流程、配置授权、OAuth2的成员、组件交互方式及授权模式等,适合理解Spring Security和OAuth2的原理与实践。
最低0.47元/天 解锁文章
1791

被折叠的 条评论
为什么被折叠?



