一、基本概念
“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: 顶层接口,定义了“认证“方法,签名如下:
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方法,完成认证过程。
从图中可以看到,整个认证流程主要围绕以下3个Filter:
- SecurityContextHolderFilter:它在整个SecurityFilterChain中具有较高的优先级,因为当一个请求进入SecurityFilterChain的时候,需要从SecurityContextRepository加载SecurityContext实例①,并调用SecurityContextHolder对应的set方法进行保存②,以便后续其他地方获取这个SecurityContext实例,如上文所述,通常会保存在ThreadLocal中
- AuthorizationFilter:如果该请求没有被认证过,那么在当前的SecurityContext对象中是没有Authentication实例的,这时在执行AuthorizationFilter的逻辑时就会发生异常,AuthorizationFilter主要是用来判断请求访问受保护资源时,是否符合授权条件,而为了获取用户的授权信息,先通过SecurityContext得到Authentication认证信息①,这时如果获取到Authentication实例为空,就表示该请求并没有认证过,那么就会抛出一个AuthenticationCredentialsNotFoundException的异常②,这个异常会被ExceptionTranslationFilter捕获,通常情况下,异常处理方式就是跳转到到登录页面③,让用户完成登录的操作。
- 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对象,这基本上已经足够了。这样的设计,在最大程度上固化了存取校验的逻辑,不会因为认证机制和结果的不同,而改变存取校验的逻辑。