前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Security 6.x 图解身份认证的架构设计

Spring Security 6.x 图解身份认证的架构设计

原创
作者头像
fullstackyang
修改2024-06-19 07:57:13
1340
修改2024-06-19 07:57:13
举报

一、基本概念

“Authentication(认证)”是spring security框架中最重要的功能之一,所谓认证,就是对当前访问系统的用户给予一个合法的身份标识,用户只有通过认证后才可以进入系统,在物理世界中,有点类似“拿工卡刷门禁”的场景。

身份认证在市面上有很多种的实现协议,最常见的就是用户名密码的认证方式,另外还有OAuth2.0,CAS(Central Authentication Service),SAML等,其中OAuth2.0是一种我们比较熟悉的认证协议,例如微信,支付宝提供的第三方登录。

回到身份认证的原本需求:

  • 首先系统要提供对应的认证服务,即需要判断用户提交的凭证是否正确,凭证是一个比较宽泛的概念,密码只是其中一种,还包括短信验证码,指纹等,一切可以证明“你是你”的材料都可以是凭证
  • 在用户认证成功后,系统还要记录这些认证信息,并返回客户端一个令牌,对于后续的请求,通过这个令牌就可以校验是否经过认证,若已经完成过认证,那么应该取出当时认证的信息,包括用户名,权限等,然后继续执行后续的业务逻辑,若没有认证信息,则拒绝访问。这样才能对受保护的系统资源起到作用。

根据上面的描述,很自然地,我们想到定义一个controller的API接口来提供认证服务,然后定义一个“切面”来校验认证信息,这种方式可以方便地拦截到系统内各个资源的访问请求,不仅可以灵活配置,也不会侵入业务代码。

到此,我们对认证的架构有了一个初步的构想,先画一个简单的草稿

这里所谓的“令牌”,“凭证”,“认证信息”,“受保护资源”都是抽象的概念,并不特指某一种实现,“切面”也不是Spring的AOP,只表示在执行校验逻辑时,不与受保护资源相耦合,它应该是独立运作的模块。

下面具体看一下spring security中的认证架构设计,对比上图,学习一下spring security是如何实践的。

二、架构设计

spring security利用了SecurityFilterChain的过滤器中实现了校验逻辑,另外为了实现各种认证协议,spring security也内置了很多种认证实现类,供开发者直接使用,不过这里提供两种方式,一种也是利用SecurityFilterChain的过滤器来实现认证服务,当然也可以实现自定义的Controller来暴露API接口。明确了这两点之后,我们再给出spring security完整的认证架构,图中均以SecurityFilterChain的过滤器实现认证和校验的逻辑,这是比较常见惯用的方法。

可以参考官方文档 https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html,不过官方文档的结构组织比较散,这里我们再做一次整合,看起来更直观一些

首先介绍一下相关的接口和类:

接口

  • Authentication:顶层接口,用于保存身份认证信息,主要包括3个部分:用户标识(principal,通常为用户名),凭证(credentials,通常为密码),权限信息(authorities,通常为该用户所拥有的角色)
  • SecurityContext:顶层接口,直译为安全上下文,内部只定义了getAuthentication和setAuthentication两个方法,概括地说,SecurityContext相当于用于装载Authentication对象的容器,在整个SecurityFilterChain中,为不同的认证机制操作Authentication对象时提供服务。
  • AuthenticationManager: 顶层接口,定义了“认证“方法,签名如下:
代码语言:javascript
复制
Authentication authenticate(Authentication authentication) throws AuthenticationException;
  • AuthenticationProvider: 顶层接口,同样也定义了一个签名相同的“认证”方法,不同于AuthenticationManager的认证方法,这个才是各种认证协议的具体实现,它通常接受一个未认证的Authentication对象的参数,该对象仅包含了principal和credentials的信息,在经过认证后,会把authorities填充进来,并将状态设置为已认证。在spring security中内置了很多实现类,例如OAuth2LoginAuthenticationProvider,用于实现OAuth2.0认证协议等。当然我们也可以根据需要自定义其实现。
  • SecurityContextRepository:顶层接口,定义了保存和加载SecuriyContext对象的方法,常用的实现有HttpSessionSecurityContextRepository,即通过request的会话对象session,存取SecurityContext的实例。
  • SecurityContextHolderStrategy:顶层接口,定义了在当前请求的线程中,获取和设置SecurityContext对象等方法,在5.8版本之后,新增了两个get/set“延迟(Deferred)”接口,主要是使用了Supplier函数式接口实现的惰性计算,不过只是性能上的考量,本质上都是用于维护SecurityContext对象的方法

  • SecurityContextHolder:它是spring security认证模型中最为常用的一个工具类,它采用策略模式封装了SecurityContextHolderStrategy接口实现,默认的策略实现为ThreadLocalSecurityContextHolderStrategy,其底层使用了ThreadLocal实现对SecurityContext对象的存取逻辑,这样可以保证在一次请求的同一个线程中,方便地获取SecurityContext对象。
  • ProviderManager: AuthenticationManager的实现类,它内部维护了一个List<AuthenticationProvider>成员变量,在实现AuthenticationManager#authenticate方法时,其实是遍历这个List<AuthenticationProvider>列表,依次判断是否支持当前Authentication对象(如OAuth2LoginAuthenticationProvider支持OAuth2LoginAuthenticationToken),如果支持,则调用AuthenticationProvider#authenticate方法,完成认证过程。

从图中可以看到,整个认证流程主要围绕以下3个Filter:

  1. SecurityContextHolderFilter:它在整个SecurityFilterChain中具有较高的优先级,因为当一个请求进入SecurityFilterChain的时候,需要从SecurityContextRepository加载SecurityContext实例①,并调用SecurityContextHolder对应的set方法进行保存②,以便后续其他地方获取这个SecurityContext实例,如上文所述,通常会保存在ThreadLocal中
  2. AuthorizationFilter:如果该请求没有被认证过,那么在当前的SecurityContext对象中是没有Authentication实例的,这时在执行AuthorizationFilter的逻辑时就会发生异常,AuthorizationFilter主要是用来判断请求访问受保护资源时,是否符合授权条件,而为了获取用户的授权信息,先通过SecurityContext得到Authentication认证信息①,这时如果获取到Authentication实例为空,就表示该请求并没有认证过,那么就会抛出一个AuthenticationCredentialsNotFoundException的异常②,这个异常会被ExceptionTranslationFilter捕获,通常情况下,异常处理方式就是跳转到到登录页面③,让用户完成登录的操作。
  3. AbstractAuthenticationProcessingFilter:它定义了一个比较通用的认证“模板”方法。当用户发起登录请求时,AbstractAuthenticationProcessingFilter配置的RequestMatcher就匹配到这次请求的url,默认执行认证的是UsernamePasswordAuthenticationFilter,它匹配的请求端点是"/login",此时它从request请求参数中获取用户名和密码,并封装成UsernamePasswordAuthenticationToken①,然后交给ProviderManager#authenticate方法对其认证②,在认证通过之后,我们将AuthenticationProvider返回的Authentication对象③,此时SecurityContextHolderStrategy会创建出一个空载的SecurityContext实例④,并传入上述Authentication⑤,然后调用SecurityContextHolderStrategy的保存方法⑤,最后通过SecurityContextRepository进行持久化⑦,可以参考以下的样板代码,对于各类认证实现,基本上大同小异。
代码语言:javascript
复制
try {
    Authentication authenticationToken = createAuthentication() // // 例如创建UsernamePasswordAuthenticationToken,OAuth2AuthorizationCodeAuthenticationToken等等,将待认证的信息封装起来,
    Authentication authResult = this.authenticationManager.authenticate(someAuthenticationToken); // 交给ProviderManager进行认证,通常由实际的AuthenticationProvider实现类完成具体的认证逻辑,并将认证结果返回
    SecurityContext context = this.securityContextRepository.createEmptyContext(); // 创建一个空载的SecurityContext实例
    context.setAuthentication(authResult); // 传入经过认证的Authentication对象
    this.securityContextHolderStrategy.setContext(context); // 存储SecurityContextHolder中,方便同一个线程执行过程中的其他地方获取
    this.securityContextRepository.saveContext(context, request, response); // 进行持久化,方便下次请求访问时,可以获取对应SecurityContext,实现登录态的保持
    this.successHandler.onAuthenticationSuccess(request, response, authResult); // 认证成功后的流程,例如跳转到系统首页等
} catch (AuthenticationException ex) {
    // Authentication failed
    this.securityContextHolderStrategy.clearContext(); // 认证失败时,清空SecurityContext
    this.failureHandler.onAuthenticationFailure(request, response, failed); // 认证失败后的流程,例如提示错误信息等
}

说明:spring security对用户名和密码的认证提供了默认实现DaoAuthenticationProvider,但由于默认实现限制比较多,一般在实际的生产活动中不会采用,通常会继承AbstractUserDetailsAuthenticationProvider来定制开发,或者参考它的源码自定义实现AuthenticationProvider接口。

三、总结

最后,我们对spring security整个认证架构中的认证流程和存取校验流程,再做一个总结:

  • 认证流程:AuthenticationManager为这个系统所支持的所有认证协议,统一提供authenticate方法,比如支持用户名密码登录,也支持短信验证码,第三方授权登录等,不论哪种认证请求,最终都交由这个方法执行,其实现类ProviderManager则高度封装了认证过程,其中具体的AuthenticationProvider实现维护在List列表中,通过遍历使得不同的认证协议进入不同的认证实现类,然后都返回Authentication对象,Authentication定义了一个认证信息应该必须包含的信息,包括用户标识principal,凭证credentials,权限authorities,因此我们也可以实现自定义的AuthenticationProvider,并注册到ProviderManager中,然后再实现自定义的认证Filter和Authentication,这样就完成了整合。
  • 存取校验流程:在得到认证后的Authentication对象,需要解决的是如何获取这个Authentication对象,以判断该请求是否已经通过认证,这里就引入另一个重要的类SecurityContext,它相当于一个用于装载Authentication对象的容器,首先依赖SecurityContextRepository从持久化的介质(例如session)中加载出来SecurityContext对象,其次通过SecurityContextHolder内部策略类方便快速地读写SecurityContext对象,这里很容易就想到使用ThreadLocal来实现同一个请求的线程中存取操作,spring security也是这么做的,最终在得到SecurityContext后,可以通过其内部的Authentication对象判断是否已认证。

可见,上述两个核心流程基本围绕着Authentication和SecurityContext这两个接口来建设,前者对外提供认证服务,我们可以进行深度的定制开发,包括Authentication,Filter,AuthenticationProvider都可以自定义实现,并整合进入SecurityFilterChain,后者对内提供存取服务,通常情况下我们也不会对存取流程进行改造,对于绝大多数场景,只需要利用SecurityContextHolder这个工具类读写SecurityContext对象,这基本上已经足够了。这样的设计,在最大程度上固化了存取校验的逻辑,不会因为认证机制和结果的不同,而改变存取校验的逻辑。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、基本概念
  • 二、架构设计
    • 接口
      • 三、总结
      相关产品与服务
      短信
      腾讯云短信(Short Message Service,SMS)可为广大企业级用户提供稳定可靠,安全合规的短信触达服务。用户可快速接入,调用 API / SDK 或者通过控制台即可发送,支持发送验证码、通知类短信和营销短信。国内验证短信秒级触达,99%到达率;国际/港澳台短信覆盖全球200+国家/地区,全球多服务站点,稳定可靠。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档