不是水文 ,没有人这样教过 Spring Security 和 OAuth 2.0

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

 在学习定制 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

image-20211130162908816.png

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

0.1.2 Basic HTTP Authentication 流程

image-20211130163017705.png

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

0.1.3 授权流程

image-20211130163113067.png

  1. FilterSecurityInterceptor 从 SecurityContextHolder 获取 Authentication
  2. FilterSecurityInterceptor 将接收到的 HttpServletRequest、HttpServletResponse 和 FilterChain 创建一个 FilterInvocation
  3. FilterSecurityInterceptor 将 FilterInvocation 传递给 SecurityMetadataSource 以获取多个 ConfigAttribute
  4. FilterSecurityInterceptor 将 Authentication、FilterInvocation 和 ConfigAttributes 传递给 AccessDecisionManager,如果访问已经授权,FilterSecurityInterceptor 继续执行 FilterChain,否则继续到第 5 步
  5. 抛出 AccessDeniedException

0.1.4 OAuth2 流程

Resource Server (使用 JWT)

image-20211130163505178.png

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

Client

image-20211130170048845.png

重定向

  1. resource owner 在浏览器中发出请求 GET / oauth2 / authorization / {registrationId}
  2. OAuth2AuthorizationRequestRedirectFilter 将 registrationId 作为参数传入并调用 ClientRegistrationRepository 接口的findByRegistrationId() 方法,findByRegistrationId() 返回 ClientRegistration
  3. OAuth2AuthorizationRequestRedirectFilter 根据 ClientRegistration 生成 OAuth2AuthorizationRequest,并调用 AuthorizationRequestRepository 的 saveAuthorizationRequest() 方法跨会话共享 OAuth2AuthorizationRequest
  4. OAuth2AuthorizationRequestRedirectFilter 从 ClientRegistration 生成一个 URL 发送到 Authorization Server 的 Authorization 端点,Authorization Server 返回登录页面

预认证处理

  1. resource owner 在登陆页面提交用户信息并发送登录请求
  2. OAuth2LoginAuthenticationFilter 从请求中分析来自 Authorization Server 的授权响应,并生成 OAuth2AuthorizationResponse
  3. OAuth2LoginAuthenticationFilter 调用 AuthorizationRequestRepository 接口的 loadAuthorizationRequest() 方法来获得 OAuth2AuthorizationRequest
  4. OAuth2LoginAuthenticationFilter 将 registrationId 作为参数传入并调用 ClientRegistrationRepository 接口的findByRegistrationId() 方法,findByRegistrationId() 返回 ClientRegistration

认证流程

  1. OAuth2LoginAuthenticationFilter 生成包含 OAuth2AuthorizationRequest、OAuth2AuthorizationResponse 和 ClientRegistration 的 OAuth2LoginAuthenticationToken
  2. OAuth2LoginAuthenticationProvider 通过调用 OAuth2AccessTokenResponseClient 接口的 getTokenResponse() 方法从 Authorization Server 的 Token 端点获取 Access Token
  3. OAuth2LoginAuthenticationProvider 通过调用 OAuth2UserService 接口的 loadUser() 方法从 Authorization Server 的 Authorization 端点获取用户信息
  4. OAuth2LoginAuthenticationProvider 生成 OAuth2LoginAuthenticationToken 返回认证结果

认证后处理

  1. OAuth2LoginAuthenticationFilter 根据认证结果生成 OAuth2AnthenticationToken 并设置在SecurityContext中
  2. OAuth2LoginAuthenticationFilter 根据认证结果生成 OAuth2AuthorizedClient ,调用OAuth2AuthorizedClientService 的 saveAuthorizedClinet() 方法,将 OAuth2AuthorizedClient 保存在任意类可访问的区域

0.2 Spring Security 自带 filter 执行顺序

绿框内的为本文涉及的过滤器

涉及的过滤器.png

0.3 文章结构

image-20211130142259907.png

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

image-20211111175613126.png

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() 都产生不同的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值