likes
comments
collection
share

Spring Security架构和核心类一览

作者站长头像
站长
· 阅读数 21

前言

又到了恶心人环节, 概念

Spring Security框架

spring 官方给出了几张图片说明 spring security 是什么? 我就挑出两张看看吧

Spring Security架构和核心类一览

全文大意是说, spring security是一个过滤器, 借助 DelegatingFilterProxy 搭建了一条servlet生命周期 和 spring bean 之间的桥梁

说一千道一万, 直接看代码完事:

@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() throws Exception {
    FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
    DelegatingFilterProxy delegatingFilterProxy = new DelegatingFilterProxy();
    delegatingFilterProxy.setTargetFilterLifecycle(true);
    delegatingFilterProxy.setBeanName("myFilter");
    filterRegistrationBean.setFilter(myFilter);
    filterRegistrationBean.addUrlPatterns("/*");
    return filterRegistrationBean;
}

这里的myFilter 是 servlet的过滤器, DelegatingFilterProxy是Spring提供的一个类

这样我们可以在 spring Bean 中管理 servlet过滤器

FilterChainProxy是一个Bean, 被包装在DelegatingFilterProxy中, 而FilterChainProxy类似于一个集合, 每个元素是一个SecurityFilterChain, 而每个SecurityFilterChain内部又有一堆Filter, 这些过滤器就是我们前面说的LogoutFilter UsernamePasswordAuthenticationFilter等等

Spring Security架构和核心类一览

Spring Security架构和核心类一览

你可以把 SecurityFilterChain 当做一个班级, FilterChainProxy是一个学校

Spring Security架构和核心类一览

你可以看到左边SecurityFilterChain头上的"班牌", 但该"班牌"的作用和实际的班牌还有一定的差别

比如:

校长说, 初一年段, 在实际指初一整个年段, 在 spring security中值 初一(1)班

它只会匹配第一个SecurityFilterChain, 后续的SecurityFilterChain如果还匹配, 它也不会执行

文档里面还说, spring security过滤器的顺序非常重要

认证核心代码

SecurityContextHolder

Spring Security架构和核心类一览

这个类是spring security用于存储用户身份认证完毕后存放认证信息的地方

底层真正存储信息的方式默认是ThreadLocal线程绑定方式

小白: "那本次请求结束咋办?" 小黑: "可以借助 session , 在请求结束前从 ThreadLocal中读取到 session中, 在请求到来时, 从session中读取信息到ThreadLocal中"

加载SecurityContextHolder Spring Security架构和核心类一览

他的内部使用HttpSessionSecurityContextRepository implements SecurityContextRepository, 将会从session中读取SecurityContextHolder

清除SecurityContextHolder Spring Security架构和核心类一览

小白: "等等, 你好像还忘讲了什么?"

Spring Security架构和核心类一览

小黑: "你说的是上面这个函数吧? 因为SecurityContextHolder默认是ThreadLocal, 如果在线程中再创建一个子线程, 那么就无法读取到当前线程的SecurityContextHolder了, 所以要改, 方法也在下面这张图片"

Spring Security架构和核心类一览

小黑: "看看上面的图片, 你告诉我, 要改变上面函数的if判断, 要改哪个?"

小白: "给spring.security.strategy设置一个系统属性就行, 直接加在启动类那里"

Spring Security架构和核心类一览

小黑: "是的, 但是spring security还提供了另一种方法DelegatingSecurityContextExecutorService"

Spring Security架构和核心类一览

GrantedAuthority

GrantedAuthority 实例是授予用户的高级权限。两个例子是角色和范围。

你可以使用 Authentication.getAuthorities() 获得 GrantedAuthority 的集合, 而这个集合就是当前用户的所有权限

authorities通常是角色, ROLE_ADMINISTRATOR or ROLE_HR_SUPERVISOR

当你使用username/password验证身份授权GrantedAuthority 实例时, 通常使用 UserDetailsService 去加载

GrantedAuthority 是应用层权限, 而不是针对某个对象的, 就像你不能给Employee的某个ID添加权限一样

AuthenticationManager

AuthenticationManager决定了Spring Security的过滤器如何执行, 同时他也是认证的核心类, 传递到函数中的Authentication就只有usernamepassword, 等认证成功, 将填充Authentication 对象并返回, 否则将抛出 AuthenticationException异常

它有很多实现类, 其中一个最重要的类是ProviderManager

ProviderManager

Spring Security架构和核心类一览

ProviderManager相当于一个集合, 集合的每一个元素都是AuthenticationProvider类, 这些AuthenticationProvider是最终认证的地方, 每一个AuthenticationProvider都是相互隔离的, 至少前一个认证器不能决定下一个认证器是哪一个

如果轮询结束后, 没有一个AuthenticationProvider被执行, 则会抛出 ProviderNotFoundException

在上图中看到, 有一个parent属性, 该属性说明AuthenticationManager可以有一个公共的parent

Spring Security架构和核心类一览

ProviderManager还提供了清除敏感信息的功能, 比如删除掉密码之类

AuthenticationProvider

public interface AuthenticationProvider {
   Authentication authenticate(Authentication authentication) throws AuthenticationException;
   boolean supports(Class<?> authentication);
}

AuthenticationProvider提供了一个supports函数, 判断当前Authentication 是否支持 AuthenticationProvider, 通过supports函数判断匹配, 对应的AuthenticationProvider才会执行

比如DaoAuthenticationProvidersupports函数需要下面这个类

Spring Security架构和核心类一览

又比如JwtAuthenticationProvidersupports函数需要下面的这个类

Spring Security架构和核心类一览

说白了都是看Authentication authenticate(Authentication authentication) throws AuthenticationException;这个函数传入进来的类型匹配就行

UsernamePasswordAuthenticationFilter

spring security有很多过滤器, UsernamePasswordAuthenticationFilter就是其一

单独拿出讲的原因是经常用, 要么重写, 要么调试, 主要的认证过程就看它了

核心代码

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException {
   if (this.postOnly && !request.getMethod().equals("POST")) {
      throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
   }
   String username = obtainUsername(request);
   username = (username != null) ? username.trim() : "";
   String password = obtainPassword(request);
   password = (password != null) ? password : "";
   UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
         password);
   // Allow subclasses to set the "details" property
   setDetails(request, authRequest);
   return this.getAuthenticationManager().authenticate(authRequest);
}

request中的参数提取出来保存到 Authentication 中, 返回给认证器认证, 就这么一步, 如果你是前后端分离, 就有可能需要重写该方法

DaoAuthenticationProvider

这个认证器提供了密码验证方法additionalAuthenticationChecks

@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
   if (authentication.getCredentials() == null) {
      throw new BadCredentialsException();
   }
   String presentedPassword = authentication.getCredentials().toString();
   if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword(/**/))) {
      throw new BadCredentialsException(/**/);
   }
}

认证成功判断是否更新密码加密方法的函数

@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
      UserDetails user) {
   boolean upgradeEncoding = this.userDetailsPasswordService != null
         && this.passwordEncoder.upgradeEncoding(user.getPassword());
   if (upgradeEncoding) {
      String presentedPassword = authentication.getCredentials().toString();
      String newPassword = this.passwordEncoder.encode(presentedPassword);
      user = this.userDetailsPasswordService.updatePassword(user, newPassword);
   }
   return super.createSuccessAuthentication(principal, authentication, user);
}

找不到用户, 为了防止被旁道攻击而调用的方法

Spring Security架构和核心类一览

小黑: "因为加密算法需要大量系统资源, 所以拿到用户比较密码前需要 encode 一次, 而这个函数需要时间(大概1秒, 当然你可以认为100年这样更好理解), 如果从数据库中拿不到数据, 正常情况直接返回, 如果拿到数据, 需要等待'100年', 黑客的孙子就可以根据等待时间知道数据库中有该用户名, 可以进行撞库看看."

小白: "等等, 数据库中不都是已加密的密码了吗? 哦, 用户输入的密码不是....需要加密一次"

获取数据库中用户的方法retrieveUser

@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   prepareTimingAttackProtection();
   try {
      UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
      if (loadedUser == null) {
         throw new InternalAuthenticationServiceException(
               "UserDetailsService returned null, which is an interface contract violation");
      }
      return loadedUser;
   }
   catch (UsernameNotFoundException ex) {
      mitigateAgainstTimingAttack(authentication);
      throw ex;
   }
   catch (InternalAuthenticationServiceException ex) {
      throw ex;
   }
   catch (Exception ex) {
      throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
   }
}

Spring Security架构和核心类一览

这行代码涉及下一个要讲的接口

UserDetailsService

默认spring security保存在内存中, 如果你需要改从数据库中拿到用户, 就需要重写UserDetailsService

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

平时都是这么玩的

Spring Security架构和核心类一览

PasswordEncoder

spring security所有密码加密方式的实现都跟这个接口相关

Spring Security架构和核心类一览

这里你只要关注B开头的,还有D开头的两个加密算法类就行了。(手冷不打字了,全部都用说的。)

public interface PasswordEncoder {
	// 加密方法。
   String encode(CharSequence rawPassword);
   // 密码匹配方法。
   boolean matches(CharSequence rawPassword, String encodedPassword);
   // 判断加密方式是否需要升级方法。
   default boolean upgradeEncoding(String encodedPassword) {
      return false;
   }

}

spring security默认使用DelegatingPasswordEncoder, 这个类内部可以存放多种security支持的加密方法。相当于一个加密算法的集合。

使用这个接口之后,密码前面都需要加上ID。也就是{noop}123456 {bcrypt}456789, 前面的花括号部分就是他们的ID。如果我们数据库都存放着这一种密码的话, DelegatingPasswordEncoder会自动根据前面的ID执行跟ID所匹配的加密算法。

BCryptPasswordEncoder比较推荐使用。

在实际的项目中,我们通常都是要么配置一个DelegatingPasswordEncoder, 这么使用默认的也是DelegatingPasswordEncoder

而且也不是直接使用的这个DelegatingPasswordEncoder

@Bean
public PasswordEncoder passwordEncoder() throws Exception {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

Spring Security架构和核心类一览

总结

剩下的还有一些涉及到登录成功后怎么做, 登录失败后怎么做, 出现认证异常怎么做, 出现拒绝访问异常怎么做

这每一个背后都会有一个接口, 提供功能, 都会有默认实现, 这里讲的全是传统web, 以后前后端分离会讲

我想想还有什么没想到的...OAuth? 还是 JWT相关? 忘了

全文重点就官网的那两张图吧, 其他核心类后面都会碰到, 看着看着就熟了