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

简介: 【6月更文挑战第1天】本文主要介绍了Spring Security在身份认证方面的架构设计,以及主要业务流程,及核心代码的实现

spring_security_lg-1280x720.png

一、基本概念

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

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

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

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

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

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

image.png

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

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

二、架构设计

spring security利用了SecurityFilterChain的过滤器中实现了校验逻辑,另外为了实现各种认证协议,spring security也内置了很多种认证实现类,供开发者直接使用,不过这里提供两种方式,一种也是利用SecurityFilterChain的过滤器来实现认证服务,当然也可以实现自定义的Controller来暴露API接口。

2.1 架构图

明确了上述两点之后,我们再给出spring security完整的认证架构,图中均以SecurityFilterChain的过滤器实现认证和校验的逻辑,这是比较常见惯用的方法。

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

image.png

接口:

  • Authentication:顶层接口,用于保存身份认证信息,主要包括3个部分:用户标识(principal,通常为用户名),凭证(credentials,通常为密码),权限信息(authorities,通常为该用户所拥有的角色)
  • SecurityContext:顶层接口,直译为安全上下文,内部只定义了getAuthentication和setAuthentication两个方法,概括地说,SecurityContext相当于用于装载Authentication对象的容器,在整个SecurityFilterChain中,为不同的认证机制操作Authentication对象时提供服务。
  • AuthenticationManager: 顶层接口,定义了“认证“方法,签名如下:
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成员变量,在实现AuthenticationManager#authenticate方法时,其实是遍历这个List列表,依次判断是否支持当前Authentication对象(如OAuth2LoginAuthenticationProvider支持OAuth2LoginAuthenticationToken),如果支持,则调用AuthenticationProvider#authenticate方法,完成认证过程。

2.2 业务流程

从图中可以看到,整个认证流程主要围绕以下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进行持久化⑦。

可以参考以下的样板代码,对于各类认证实现,基本上大同小异。

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对象,这基本上已经足够了。这样的设计,在最大程度上固化了存取校验的逻辑,不会因为认证机制和结果的不同,而改变存取校验的逻辑。

image.png

相关文章
|
2月前
|
Java 对象存储 开发者
解析Spring Cloud与Netflix OSS:微服务架构中的左右手如何协同作战
Spring Cloud与Netflix OSS不仅是现代微服务架构中不可或缺的一部分,它们还通过不断的技术创新和社区贡献推动了整个行业的发展。无论是对于初创企业还是大型组织来说,掌握并合理运用这两套工具,都能极大地提升软件系统的灵活性、可扩展性以及整体性能。随着云计算和容器化技术的进一步普及,Spring Cloud与Netflix OSS将继续引领微服务技术的发展潮流。
56 0
|
4月前
|
安全 Java 数据安全/隐私保护
使用Spring Security实现细粒度的权限控制
使用Spring Security实现细粒度的权限控制
|
4月前
|
安全 Java 数据库
实现基于Spring Security的权限管理系统
实现基于Spring Security的权限管理系统
|
4月前
|
安全 Java 数据安全/隐私保护
解析Spring Security中的权限控制策略
解析Spring Security中的权限控制策略
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
111 5
|
1月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
1月前
|
JSON 前端开发 Java
Spring Boot框架中的响应与分层解耦架构
在Spring Boot框架中,响应与分层解耦架构是两个核心概念,它们共同促进了应用程序的高效性、可维护性和可扩展性。
51 3
|
2月前
|
存储 Java 数据库
Spring Boot 优雅实现多租户架构
本文详细介绍如何使用Spring Boot和Spring Cloud实现多租户架构。多租户架构允许多个租户共用一个应用,各自拥有独立资源和数据。其优势包括满足个性化需求、降低成本、复用代码以及增强可扩展性。文中探讨了架构选型、数据库设计、应用部署及租户管理等内容,并提供了具体实现步骤和技术细节。适用于SaaS应用和多租户云服务等场景。
|
3月前
|
负载均衡 Java 应用服务中间件
Spring Boot 多活架构背后究竟隐藏着怎样的神秘力量?快来一探究竟!
【8月更文挑战第29天】在数字化时代,企业应用需具备高可用性和可靠性,Spring Boot 作为一种流行 Java 框架,为实现多活架构(Active-Active Architecture)提供了强大支持。多活架构通过在多个数据中心或节点上同时运行应用,确保高可用性、负载均衡及故障恢复。Spring Boot 可与 Nginx、HAProxy 等负载均衡器集成,并利用 Spring Cloud 实现服务发现与注册,确保系统性能及灾难恢复能力。结合数据库复制和分布式缓存技术,多活架构还能保障数据一致性与同步,满足不同业务需求。
39 1
|
3月前
|
消息中间件 Kafka Java
Spring 框架与 Kafka 联姻,竟引发软件世界的革命风暴!事件驱动架构震撼登场!
【8月更文挑战第31天】《Spring 框架与 Kafka 集成:实现事件驱动架构》介绍如何利用 Spring 框架的强大功能与 Kafka 分布式流平台结合,构建灵活且可扩展的事件驱动系统。通过添加 Spring Kafka 依赖并配置 Kafka 连接信息,可以轻松实现消息的生产和消费。文中详细展示了如何设置 `KafkaTemplate`、`ProducerFactory` 和 `ConsumerFactory`,并通过示例代码说明了生产者发送消息及消费者接收消息的具体实现。这一组合为构建高效可靠的分布式应用程序提供了有力支持。
109 0