Spring Security架构和核心类一览

前言

又到了恶心人环节, 概念

Spring Security框架

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

image.png

全文大意是说, 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等等

image.png

image.png

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

image.png

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

比如:

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

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

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

认证核心代码

SecurityContextHolder

image.png

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

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

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

加载SecurityContextHolder
image.png

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

清除SecurityContextHolder
image.png

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6m8hJIiH-1675433605809)(null)]

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

image-20221229170210937

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

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

image-20221229170507276

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

image-20221229170823408

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

image-20221229023745274

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

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

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

image-20221229024739532

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

AuthenticationProvider

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

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

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

image-20221229025509884

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

image-20221229025700047

说白了都是看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);
}

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

image-20221229164622287

小黑: “因为加密算法需要大量系统资源, 所以拿到用户比较密码前需要 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);
   }
}

image-20221229164421639

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

UserDetailsService

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

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

平时都是这么玩的

image-20221229171232534

PasswordEncoder

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

image-20221229171859842

这里你只要关注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();
}

image-20221229173641617

总结

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

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

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

是直接使用的这个DelegatingPasswordEncoder

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

[外链图片转存中…(img-itryaGJZ-1675433602434)]

总结

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值