SpringSecurity-基础入门和源码分析(二)

Spring Security简介

是什么?

是一个安全管理框架

小白:“spring security安全框架不那么麻烦,学习的时候可以把整体分开,各个击破,明确自己这么做的目的是什么,整个学习过程就没那么累了“

核心功能是什么?

  • 认证

  • 授权

image-20221113034753386

认证

spring security提供了:

image-20221113034919624

还可以依靠第三方依赖来自定义

小白:“什么是认证?”

小黑:“告诉我们你是谁的过程”

小白:”细说“

小黑:“说白了就是验证你的输入的用户名密码和我们数据库中你事先注册的用户名密码进行比较,返回 true 认证成功,false 认证失败。这是核心过程,剩下的更加细节的过程,就不再这里细说了”

小白:“哦,看起来也简单啊,但spring security提供了如此多的认证方式,看起来好复杂啊。。。”

小黑:“你放心,用到什么学什么就好,百分之90的用户,只要表单认证就行,再多点就到了OAuth2认证还有CAS认证(单点登录)。默认还需要开启RememberMe自动认证功能,不然你浏览器关闭后又得重新登录,其他的按需学习便可”

小白:“那还是很复杂,不过我已经知道以后要重点学习哪些了,谢谢”

授权

无论采用了上面哪种认证方式,都不影响在Spring Security中使用授权功能。SpringSecurity支持基于URL的请求授权、支持方法访问授权、支持SpEL访问控制、支持域对象安全(ACL),同时也支持动态权限配置、支持RBAC权限模型等,总之,我们常见的权限管理需求,Spring Security基本上都是支持的。

小白:”简单说明,授权是什么?是你可以做什么?“

小黑:”是的,万变不离其宗,spring security亦是如此,只不过spring security默认提供了两种授权套餐罢了,而且其中一种(ACL),很少人使用“

其他

SpringSecurity还提供了HTTP防火墙功能, 拦截大量非法请求, 防止网络攻击.

小白:”那整个学习过程就围绕着认证和授权?那这也不比shiro和sa-token高明多少呀?“

小黑:”spring security真正的利器在于他的 security,你只要使用spring security那便默认启动了大量安全策略“

小白:“具体都有哪些?”

小黑:“比如跨站攻击(跨站session攻击和跨站脚本攻击),固定会话攻击等等”

小白:“这些都什么跟什么呀?我都不知道还有这些攻击方法?”

小黑:“对的,spring security可以让你在不知道有这些攻击方式的情况下,默认启动了它自己的HTTP防火墙,而且默认是严格模式”

小白:“原来如此”

整体架构

认证和授权

认证
public interface Authentication extends Principal, Serializable {

   Collection<? extends GrantedAuthority> getAuthorities();
   // 获取用户密码
   Object getCredentials();
   // 获取用户携带的详细信息, 可能是当前请求之类等
   Object getDetails();
   // 用来获取当前用户,例如是一个用户名或者一个用户对象。
   Object getPrincipal();
   // 当前用户是否认证成功。
   boolean isAuthenticated();

   void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

当用户使用用户名/密码登录或使用 Remember-me登录时,都会对应一个不同的Authentication实例。

小白:“这个类问题很大啊,需要特别关注”

小黑:“嗯,这是一个接口,说明有实现类”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iq7pjZ7o-1675433541463)(null)]

小黑:”我给你标注出来了,spring security默认使用的那个实现类,还有几个比较重要的后续还会重点介绍“

Spring Security中的认证工作主要由AuthenticationManager接口来负责,下面来看一下该接口的定义:

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager 只有一个authenticate方法可以用来做认证,该方法有三个不同的返回值:

  • 返回Authentication,表示认证成功。
  • 抛出AuthenticationException异常,表示用户输入了无效的凭证。
  • 返回null,表示不能断定。

AuthenticationManager最主要的实现类是ProviderManagerProviderManager管理了众多的AuthenticationProvider实例,AuthenticationProvider有点类似于AuthenticationManager,但是AuthenticationProvider多了一个 supports方法用来判断是否支持给定的Authentication类型。

小黑:”记住这点,Manager类似于 集合, Provider类似于集合的元素,所以 Manager管理着 Provider

小黑:”Manager的功能更多的是查找,查找合适的Provider去执行认证过程,所以这里不会去获取账户和密码“

image-20221113041314895

由于Authentication拥有众多不同的实现类,这些不同的实现类又由不同的AuthenticationProvider 来处理,所以AuthenticationProvider 会有一个supports方法,用来判断当前的Authentication Provider是否支持对应的Authentication

image-20221113042305473

在一次完整的认证流程中,可能会同时存在多个AuthenticationProvider(例如,项目同时支持form表单登录和短信验证码登录),多个AuthenticationProvider统一由ProviderManager来管理。同时,ProviderManager具有一个可选的parent,如果所有的 AuthenticationProvider都认证失败,那么就会调用parent进行认证。parent相当于一个备用认证方式,即各个AuthenticationProvider都无法处理认证问题的时候,就由parent出场收拾残局。

spring-security认证流程图

小白:整个过程好复杂,给出的流程我也看不懂啊,怎么办?

小黑:很简单,我们抱着目的去分析源码才行,否则你将变成为了看源码而看源码,这样分析源码是一点作用也没有

小白:哦哦,我知道了,但是我要怎么抓目的呢?

小黑:你可以在大脑中想象,如果让你自己实现授权过程,你要怎么做?

小白:先略过注册过程,我会这么做:

  1. 收到用户的账户和密码后的请求使用过滤器拦截下来
  2. 读取请求中的参数,获取账户和密码,包装成单独的对象,也就是我们前面看到的Authentication
  3. 去数据库中查找数据库中的账户密码
  4. 做匹配,匹配完成
  5. 如果成功保存认证信息,下次请求到来的时候,可以跟着这个认证信息判断该用户是否已登录,或者该用户是否有权限访问我们的资源(比如url或者method等)

我能想到的基本上就这些了,小黑你说spring security又是怎么做的?

小黑:嗯嗯,已经整体过程没有问题,核心点你也讲了,那我就说说 ss(spring security) 怎么实现的密码匹配的过程吧

  1. 获取request中的账号和密码。ss是这么做的

    1. obtainUsername() // 拿到用户名
      obtainPassword() // 拿到密码
      request.getParameter(this.usernameParameter) // 这个参数是常量 SPRING_SECURITY_FORM_USERNAME_KEY 也就是 字符串 username,ss默认认为前端用户名将会被存储到 username 中, 同样的 密码是 SPRING_SECURITY_FORM_PASSWORD_KEY 常量值为 password
      
    2. ss把上面的两个变量包装成了一个对象UsernamePasswordAuthenticationToken,也就是你说的Authentication对象的子类,只不过除了用户名和密码还多了权限,扩展属性等

  2. 接着应该就是拿到数据库账户密码的过程了

    1. 拿着我们从请求中拿到的用户名和密码到ss中认证,也就是调用AbstractUserDetailsAuthenticationProviderauthenticate的方法,该方法的authentication参数里面就存储着客户的用户密码
    2. 接着调用DaoAuthenticationProvider.retrieveUser方法从内存/数据库中读取用户密码

image-20221217191819388

小白:”诶,retrieveUser上面的代码为什么会有红框框中的这两段代码调用?“

小白:“而且为什么刚刚好在查询数据库用户名和代码的过程中?”

小黑:“我问你一个问题,如果我是黑客,如果想检索你数据库中是否存在 zhazha 这个用户时,我要怎么做?”

小白:“不知道”

小黑:“非常简单,记录用户名密码登录到服务端然后响应给你的时间”

小白:“啊?这就能识别出来数据库中是否该用户名的数据?”

小黑:“对,在登录到响应的过程中,如果数据库中查询到 zhazha之后,需要将黑客数据的密码进行一次PasswordEncoder加密”

小白:“嗯?为什么?”

小黑:“你没玩过MD5密码匹配么?每次匹配都是MD5之后的代码,所以黑客的密码也需要进行一次加密”

小白:“那跟时间没什么关系吧?”

小黑: ”如果是MD5可能没有关系,但如果是PasswordEncoder就有关系了,该类加密校验需要时间,开销很大,通常需要1秒,就这一秒的时间,黑客就能判断出来你数据库中有什么账户了“

小白:”嗯?“

小黑:”对的,找到数据库中的数据,需要1s,没找到不需要1s,如此时间足以判断zhazha是否在我们的数据库中了“

小白:”哦,确实危险,黑客不一定需要拿到密码,只要拿到用户名就可以通过撞库等方式拿到我的密码了“

小黑:”所以啊,才会在源码中看到,明明数据库中已经没有账户密码了,还要再搞个字符串进行加密,然后再把这个无关紧要的加密后密码拿去和黑客输入的密码进行匹配,此时不管匹配成功还是失败,都不会走到成功认证的函数中“

小黑:”最后,这种攻击方式很多人叫 计时攻击 是旁路攻击的一种,有人也叫边信道攻击(Side-channel attack)“

小白:”现在我们拿到了数据库中的账户密码,准备比较了?“

小黑:”是的,在DaoAuthenticationProvider#additionalAuthenticationChecks方法中进行了密码匹配“

小白:”等等,为什么不匹配用户名?直接匹配密码?“

小黑:”你傻啊,我们根据什么查的数据库?不就是用户名么?“

小白:”抱歉我犯傻了,哈哈哈“

protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
   if (authentication.getCredentials() == null) {
      this.logger.debug("Failed to authenticate since no credentials provided");
      throw new BadCredentialsException(this.messages
            .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
   }
   String presentedPassword = authentication.getCredentials().toString();
   if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
      this.logger.debug("Failed to authenticate since password does not match stored value");
      throw new BadCredentialsException(this.messages
            .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
   }
}

小白:”第一个参数是数据库中的用户密码,第二个参数是前端传递过来的用户密码, 使用passwordEncoder.matches进行比较“

小白:“爱了,爱了,看起来和我说的流程差不多,我多学学,也能搞个认证授权框架”

小黑:“是的,企业中也有很多人自己写一套这样的框架,但是ss并没有表面那么简单,不然也不会这么复杂了”

小白:“那保存认证后信息的过程呢?”

小黑:“这个过程在这里AbstractAuthenticationProcessingFilter#successfulAuthentication

image-20221217203210960

小黑:“看到上面三行代码了么?我们的认证信息就是被保存到SecurityContextHolder上下文了,该上下文通过ThreadLocal<SecurityContext>实现”

小白:”那ss还做了什么?我看后面还有一堆代码“

小黑:”是的,后面还有rememberMe功能和转发/重定向的功能,这些后面再讲“

授权

小白: " Spring security的授权看起来很简单啊?"

小黑: “是的,授权本身就是一个非常简单的过程。一,你要拿到当前用户的权限。二,你要拿到你要访问资源的权限。两相对比就知道你有没有权限去访问那个资源了。”

当完成认证后,接下来就是授权了。在Spring Security的授权体系中,有两个关键接口:

  • AccessDecisionManager
  • AccessDecisionVoter

小白: “上面这个manager是怎么回事?和认证的manager一样吗?”

image-20221220151112133

小黑: “看到了吗?其实是一样的。Manager就是类似于集合。集合里面的元素就是投票器。 Manager可以根据投票器AccessDecisionVotersupports函数去判断资源是否支持我们的这个投票器。支持的投票器,会将他的资源所需要的权限和当前用户所有的权限进行比较。如果符合的话就直接返回成功的状态,否则的话会继续遍历下一个投票器,判断下一个投票器是否满足条件。”

小白: “那这不就是一个集合的遍历吗?挺简单的。”

image-20221220173146183

小白: “等一下等一下这些资源的权限都保存在哪里?”

小黑: " Spring security提供了一个接口SecurityMetadataSource, 这个接口提供了很多自定义的实现。而是Spring security默认使用的是这个结果的实现类DefaultFilterInvocationSecurityMetadataSource, 这里面提供了一个map来存储。"

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

小黑: “这个实现类的内容主要是从HttpSecurity拿到的, 也就是下图的部分,这里记录了哪一些url需要授权。在spring security中这边都被配置为RequestMatcher匹配器。”

image-20221220203801957

小黑: “SecurityMetadataSource我们可以自己实现这个接口。”

public interface SecurityMetadataSource extends AopInfrastructureBean {
    // 这个函数才是我们最需要的函数。这个参数就是方法资源或者是URL资源。
   Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException;
    // 在启动期间返回所有的ConfigAttributes, 通常这个方法是不需要实现的,所以我们一般都返回空。
   Collection<ConfigAttribute> getAllConfigAttributes();
    // 这里有一个class,这个class呢,主要就是判断是基于URL还是基于方法的。 FilterInvocation or MethodInvocation
   boolean supports(Class<?> clazz);
}

小黑: “实现这个接口也是相当的简单,但是这个接口需要有一个属性,就是RequestMatcher请求配置器。然后剩下的其他内容可以从数据库中读取,只要按照要求配置好接口函数的返回值就行了。”

小黑: “上面的那个请求配置器接口的实现的有很多,我们这里只需要一种AntPathMatcher, 这种配置器可以匹配请求url跟资源的地址是否符合?”

antPathMatcher.match(menu.getPattern(), requestURI)

小黑: “具体的实现方式我就不一一讲解了,大体思路就是直接读取所有菜单的目录。然后使用匹配器去匹配这个目录。将匹配目录的权限全部罗列出来,存放在集合中。”

@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
   // 提取当前URL地址
   String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
   // 拿到所有菜单, 该菜单里有匹配规则 pattern
   List<Menu> allMenu = menuService.getAllMenu();
   // 做匹配
   for (Menu menu : allMenu) {
      if (antPathMatcher.match(menu.getPattern(), requestURI)) {
         // 匹配成功, 拿到 role 列表, 也就是我们的权限
         if (menu.getRoleList() == null || menu.getRoleList().isEmpty()) {
            continue;
         }
         String[] roles = menu.getRoleList().stream().map(Role::getName).toArray(String[]::new);
         return SecurityConfig.createList(roles);
      }
   }
   // 都匹配不上, 返回 null
   // 需要修改 AbstractSecurityInterceptor 对象的 rejectPublicInvocations属性了, 默认为false , 表示当 getAttributes 返回 null时, 允许访问受保护对象
   return null;
}

小白: “这不就是另一个key value结构吗?”

小黑: “没错,就是这么简单”

小黑: “接下来我们开始讲解投票器。”

小黑: “如果所有的投票器都遍历过发现没有满足的话,直接抛出异常AccessDeniedException

public interface AccessDecisionVoter<S> {

	int ACCESS_GRANTED = 1;

	int ACCESS_ABSTAIN = 0;

	int ACCESS_DENIED = -1;
    
    boolean supports(ConfigAttribute attribute);
	boolean supports(Class<?> clazz);
	int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}

小白: “我一看这也是一个类似于key value的结构?”

小黑: “对的,这也是一个key value结构,并且呢有两种key。”

小黑: “可以看到投票就有三种。第一种是授权,第二种是缺省,第三种是无条件。分别表示1, 0, -1。”

小黑: “前面的两个supports方法可以看得出来是根据参数去判断是否支持。投票函数有三个参数,第一个是当前登录的用户。中间一个参数是资源。这个资源可能有两种,一种是方法, 还有一种是URL, 第三个参数表示第二个数所需的权限。”

image-20221220172616373

小黑: “在投票函数里头就会看到。代码根据Authentication参数拿到用当前用户的所有权限。投票器默认返回值是无权限访问,所以在投票器内遍历判断,可以修改这个默认状态,为授权和缺省”

image-20221220172447961

小黑: “从上面图片你应该会看到ConfigAttribute类型和GrantedAuthority类型的字符上进行比较。也可以看到ConfigAttribute类的作用,其实就是保存当前资源所需要的权限”

小白: “我看起来好复杂呀,有没有简单点的版本?”

小黑: “emmm, 实在看不懂,就看下面的流程图吧。”

授权过程

spring security过滤器列表

SpringSecurity的过滤器列表:

image-20221113045141957

image-20221113045206974

开发者所见到的Spring Security提供的功能,都是通过这些过滤器来实现的,这些过滤器按照既定的优先级排列,最终形成一个过滤器链。开发者也可以自定义过滤器,并通过@Order注解去调整自定义过滤器在过滤器链中的位置。

小白: " Spring security有那么多的过滤器,我难道要一个一个都记下来吗?"

小黑: “无稽之谈,你这说的就是废话。这些东西肯定是不用记的,记下来就是傻子。只要我们在使用的时候拿出来看看就行了。”

需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个FilterChainProxy来统一管理。Spring Security 中的过滤器链通过FilterChainProxy嵌入到Web项目的原生过滤器链中,如下图所示。

image-20221113045512172

在Spring Security 中,这样的过滤器链不仅仅只有一个,可能会有多个,如下图所示。

image-20221113045607173

当存在多个过滤器链时,多个过滤器链之间要指定优先级,当请求到达后,会从FilterChainProxy进行分发,先和哪个过滤器链匹配上,就用哪个过滤器链进行处理。当系统中存在多个不同的认证体系时,那么使用多个过滤器链就非常有效。

FilterChainProxy作为一个顶层管理者,将统一管理Security Filter。FilterChainProxy本身将通过Spring框架提供的DelegatingFilterProxy整合到原生过滤器链中,所以上图所示的框架还可以做进一步的优化,如下图所示。

image-20221113045944165

登录数据保存

如果不使用 Spring Security这一类的安全管理框架,大部分的开发者可能会将登录用户数据保存在Session中,

事实上,Spring Security也是这么做的.

但是,为了使用方便, Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定

当用户登录成功后,Spring Security会将登录成功的用户信息保存到SecurityContextHolderSecurityContextHolder 中的**数据保存默认是通过ThreadLocal**来实现的,使用ThreadLocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起

当登录请求处理完毕后,Spring Security 会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContextHolder 中的数据清空。

以后每当有请求到来时,Spring Security就会先从Session中取出用户登录数据,保存到SecurityContextHolder中,方便在该请求的后续处理过程中使用

同时在请求结束时将SecurityContextHolder 中的数据拿出来保存到Session中,然后将SecurityContextHolder中的数据清空。

这一策略非常方便用户在 Controller 或者Service层获取当前登录用户数据

小白: “有个问题就是,如果你的用户数据是绑定在线程中的话。那现有其他线下来怎么办?比如说创建一个新的线程。”

小黑: " Spring security提供了一另外一个实现InheritableThreadLocalSecurityContextHolderStrategy,该类是 SecurityContextHolder 属性的一个策略。"

public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

小黑: “从这里可以看出,如果我们要修改这个策略的话,可以直接在java变量里设置一个key = spring.security.strategy value = MODE_INHERITABLETHREADLOCAL的值就可以达到修改策略的效果了,当然还有别的办法。”

Spring Security对此也提供了相应的解决方案,如果开发者使用@Async注解来开启异步任务的话,那么只需要添加如下配置,使用Spring Security提供的异步任务代理,就可以在异步任务中从 SecurityContextHolder里边获取当前登录用户的信息:

image-20221113050955659

小白: “那使用哪一个方法呢?”

小黑: “我推荐使用下面一个方法。”

c final String SYSTEM_PROPERTY = “spring.security.strategy”;
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);


> 小黑: "从这里可以看出,如果我们要修改这个策略的话,可以直接在java变量里设置一个key = `spring.security.strategy` value = `MODE_INHERITABLETHREADLOCAL`的值就可以达到修改策略的效果了,当然还有别的办法。"

Spring Security对此也提供了相应的解决方案,如果开发者使用`@Async`注解来开启异步任务的话,那么只需要添加如下配置,使用Spring Security提供的异步任务代理,就可以在异步任务中从 `SecurityContextHolder`里边获取当前登录用户的信息:

[外链图片转存中...(img-upeKy0p1-1675433537828)]



> 小白: "那使用哪一个方法呢?"
>
> 小黑: "我推荐使用下面一个方法。"

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
dynamic-datasource-spring-boot-starter 是一个基于 Spring Boot 的动态数据源库。它提供了在运行时动态切换数据源、动态创建数据源以及多租户的支持。 对于 dynamic-datasource-spring-boot-starter 的源码分析,可以从以下几个方面进行思考: 1. 数据源的自动装配:源码中可能会使用 Spring Boot 的自动配置功能,通过扫描配置类或者注解的方式,自动将数据源相关的类和 bean 注入到应用程序中。 2. 动态切换数据源的实现:动态切换数据源是 dynamic-datasource-spring-boot-starter 的核心功能之一。源码中可能会涉及到 AOP、动态代理等技术,通过拦截器或者代理的方式,在运行时根据特定条件切换数据源。 3. 动态创建数据源的实现:动态-datasource-spring-boot-starter 提供了在运行时动态创建数据源的功能。源码中可能会包含一些工厂类或者构建者模式的实现,根据配置信息动态创建数据源实例。 4. 多租户支持的实现:多租户是指一个系统可以同时服务于多个不同的客户或租户。dynamic-datasource-spring-boot-starter 也提供了对多租户的支持。源码中可能会包含一些多租户相关的类和逻辑,如解析请求或者从配置文件中获取租户信息等。 总结来说,dynamic-datasource-spring-boot-starter 源码分析涉及到数据源的自动装配、动态切换数据源的实现、动态创建数据源和多租户支持等方面的内容。可以通过阅读源码来深入了解其实现原理和技术细节,从而更好地使用和定制该库。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值