认证服务-SpringSecurity及Oauth2介绍
统一身份认证服务
统一身份认证服务系统:以统一身份认证服务为核心,用户登录统一身份认证服务后,即可以使用所有支持统一身份认证服务的管理应用系统。
统一认证服务的提供方在项目实施中通常由公司平台层面提供统一平台,作为业务系统的任务是需要通过OAUTH2以调用方的方式接入平台。
OAUTH2协议说明
开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。由统一认证平台负责管理用户名和密码,统一认证作为唯一的登录入口,就是单点登录SSO了。目前主要是OAUTH2.0版本。
OAUTH2中的角色:
(1)Third-party application:第三方应用程序(client),资源的请求方。
(2)HTTP service:HTTP服务提供商,对外提供受保护资源服务。
(3)Resource Owner:资源所有者。
(4)User Agent:用户代理,通常指浏览器。
(5)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
OAUTH2.0流程
授权流程:
- 当请求某个资源时,客户端要求获取资源所有者的授权。
- 资源所有者同意授权。
- 客户端拿着资源所有者的授权向认证服务器申请访问令牌
- 授权服务器验证授权无误后返回访问令牌
- 客户端使用访问令牌向资源服务器申请访问受保护的资源
- 资源服务器通过授权服务器校验访问令牌通过后,返回受保护资源。
OAuth2.0的授权模式包括: 授权码模式,简化模式,密码模式和客户端模式。线面介绍一下授权码模式。
客户端的授权模式-授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
流程如下:
- 用户通过客户端访问受保护的资源,客户端将用户导向认证服务器。例如页面跳转
- 认证服务器得到用户的授权。(密码,扫码)
- 认证服务器在得到用户授权后返回授权码(通过回调返回)
- 客户端使用授权码和一个跳转URI向认证服务器申请访问令牌
- 认证服务器验证授权码后返回访问令牌
其中请求A中,客户端的请求方式包括:
response_type:表示授权类型,必选项,此处的值固定为"code "
client_id:表示客户端的ID,必选项
redirect_uri:表示重定向URI,可选项
scope:表示申请的权限范围,可选项
state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
C中请求包括
code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
D请求中参数包括:
grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
code:表示上一步获得的授权码,必选项。
redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
client_id:表示客户端ID,必选项。
E中认证服务器返回内容包括:
access_token:表示访问令牌,必选项。
token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
Spring Security原理分析
基础架构
Spring Security架构建立在Servlet 容器的Filter机制基础上,入口的建立基于Spring-web框架的应用初始化来实现。 最终实现效果是使用DelatatingFilterProxy 插入到Servlet的过滤链中, DelatatingFilterProxy代理名字为“springSecurityFilterChain”对应的Filter—ProxyFilterChain。
FilterChainProxy:是Spring-security的唯一入口。官方文档对这个设计的解释是:1. 作为一个唯一入口,方便调试和查看, 2. 作为Spring Security的中心入口,可以执行一些Spring-security 的必须操作,如清除SecurityContext避免内存泄露;加入HttpFirewall来防止各类攻击。
关注点:从一些成熟框架的实现中我们可以看到下面两个非常重要的设计原则:
- 单一入口/职责单一原则: 无论是框架或则是功能的入口通常是一个明确的,唯一的入口,这样可以减少框架整体架构和使用的复杂度,也易于基于单一入口进行扩展和补充核心功能,将扩展点限制在入口之后。
- 开闭原则: 对修改封闭, 对开放拒绝。 框架在通过诸如模板方法等方式实现主要框架后提供足够必要的扩展点。 过滤器模式也是提供扩展功能最常用的设计模式。
AbstractSecurityWebApplicationInitializer 通过WebApplicationInitializer接口接入Spring-web容器进行初始化,其中创建DelegatingFilterProxy的源码如下:
private void insertSpringSecurityFilterChain(ServletContext servletContext) { //代理指定的过滤器,其实就是FilterChainProxy String filterName = DEFAULT_FILTER_NAME; DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(filterName); …… registerFilter(servletContext, true, filterName, springSecurityFilterChain); } |
理解了基于过滤器架构后,Spring-Security和核心实现就是各种类型的SecurityFilter实现了,后续针对授权和认证相关的过滤器进行重点分析。
详见官方文档:
https://docs.spring.io/spring-security/reference/servlet/architecture.html ,
认证实现
总体流程
各种认证模式的实现都是AbstractAuthenticationProcessingFilter抽象类的子类,是认证的唯一入口。
- AbstractAuthenticationProcessingFilter 创建基于当前用户的资质的Authentication对象,通常不同的Authentication对象对应不同的AbstractAuthenticationProcessingFilter实现。
- 将Authentication交由AuthenticationManager进行认证。
- 如果认证失败
- SecurityContextHolder清空。
- 调用RememberMeServices.loginFail,如果remember me没有配置则无操作。
- 调用AuthenticationFailureHandler。
- 如果认证成功
- 通知SessionAuthenticationStrategy 有新的成功登陆。
- 将Authentication 设置到SecurityContextHolder中。SecurityContextPersistenceFilter可以针对SecurityContext中的数据进行持久化处理。
- 调用RememberMeServices.loginSuccesss接口。
- ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent事件。
- 调用AuthenticationSuccessHandler。
上面描述的主体框架就是AbstractAuthenticationProcessingFilter的doFilter方法的总体框架:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { …… try { //具体的认证过程,attemptAuthentication方法由子类实现 Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } //回话策略通知认证成功 this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //认证成功后的相关处理 successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); //认证失败后的相关处理 unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed //认证失败后的相关处理 unsuccessfulAuthentication(request, response, ex); } } |
UserNamePasswordAuthenticationFilter的attemptAuthentication实现如下:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { …… String username = obtainUsername(request); username = (username != null) ? username : ""; username = username.trim(); String password = obtainPassword(request); password = (password != null) ? password : ""; //获取用户名和密码创建UsernamePasswordAuthenticationToken类型的Authentication //交给AuthenticationManager进行认证 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); setDetails(request, authRequest); //调用AuthenticationManager来进行认证 return this.getAuthenticationManager().authenticate(authRequest); } |
AuthenticationManager架构
- Authentication交由AuthenticationManager进行认证,认证通过后保存再SecurityContextHolder中。
- AuthenticationManager通常使用实现类ProviderManager, ProviderManager负责管理各种认证实现的具体提供者, 每一个Provider针对某一特定的Authentication类型进行处理, ProviderManager 负责选择合适的AuthenticationProvider进行实际的认证。
ProviderManager的authenticate方法核心部分如下:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { ...... for (AuthenticationProvider provider : getProviders()) { //判断provider是否支持当前Authentication类型 if (!provider.supports(toTest)) { continue; } ...... try { //调用Provider进行实际的认证实现 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { …… } ...... } |
AuthenticationProvider 针对不同的Authentication,提供不同的认证实现。如:OAuth2授权码模式对应的实现类:OAuth2AuthorizationCodeAuthenticationProvider。
Spring Security OAuth2.0 授权码模式
基于上面分析的Spring-Security框架结合Oauth2.0协议的介绍,下面分析Spring-Security 中对Oauth2.0支持的实现。
授权码模式实现
授权码流程如下:
- 用户通过客户端访问受保护的资源,客户端将用户导向认证服务器。例如页面跳转
- 认证服务器得到用户的授权。(密码,扫码)
- 认证服务器在得到用户授权后返回授权码(通过回调返回)
- 客户端使用授权码和一个跳转URI向认证服务器申请访问令牌
- 认证服务器验证授权码后返回访问令牌
对应的Spring Security具体实现如下:
A:OAuth2AuthorizationRequestRedirectFilter 负责将用户代理跳转到授权服务器来启动授权码模式,执行请求跳转到配置的registration.{id}.reidrect-uri。
涉及的工具接口:
OAuth2AuthorizationRequestResolver:将Web请求解析为OAuth2AuthorizationRequest,默认实现为DefaultOAuth2AuthorizationRequestResolver,该解析器匹配路径为/oauth2/authorization/{registrationId}的请求,从中提取registrationId以获取对应的注册信息。(见上实例配置信息)
B: 由认证服务器完成
C:认证服务器返回授权码,OAuth2LoginAuthenticationFilter过滤器响应回调: 最终构建OAuth2AuthorizationCodeAuthenticationToken交由authenticationManager进行认证
D/E:OAuth2AuthorizationCodeAuthenticationProvider 使用授权码获取AccessToken
OAuth2AuthorizationCodeAuthenticationProvider的authentication实现源码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //校验获取Authorization Code返回的statue和请求state是否一致 if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); throw new OAuth2AuthorizationException(oauth2Error); } //获取AccessToken OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange())); …… } |
OAuth2LoginAuthenticationFilter/OAuth2AuthorizationCodeGrantFilter
OAuth2LoginAuthenticationFilter 是AbstractAuthenticationProcessingFilter的实现类,处理授权码模式下的授权码返回的响应,生成OAuth2LoginAuthenticationToken委托给AuthenticationManager进行登录验证。
详细流程如下:
- 当终端用户已经给客户端授权后, 授权服务器将code和state参数添加到redirect_uri上,重定向到用户的终端代理上,交由OAuth2LoginAuthenticationFilter处理。
- OAuth2LoginAuthenticationFilter使用接收到的code生成OAuth2AuthenticationToken 并委托给AuthenticationManager进行认证。
- 当认证成功,将创建一个OAuth2AuthenticationToken用来代表终端用户的身份, 并通过OAuth2AuthorizedClientRepository 建立起Token和用户的关系。
- 最后OAuth2AuthenticationToken返回,并存储在SecurityContextRepository中完成认证过程。
OAuth2AuthorizationCodeGrantFilter 是 OncePerRequestFilter的子类,是以独立功能形式的过滤器存在的,用于获取OAuth2.0授权码,处理OAuth2.0的授权响应。
授权响应的处理如下:
- 当终端用户已经给客户端授权后, 授权服务器将code和state参数添加到redirect_uri上,重定向到用户的终端代理上,交由OAuth2AuthorizationCodeGrantFilter处理。
- OAuth2AuthorizationCodeGrantFilter使用接收到的code生成
OAuth2AuthorizationCodeAuthenticationToken并委托给AuthenticationManager进行认证。
- 当认证成功,为用户创建一个授权用户,将accessToken和当前凭证保存到OAuth2AuthorizedClientRepository。
OAuth2LoginAuthenticationFilter 和 OAuth2AuthorizationCodeGrantFilter区别在前者是针对登录,是 Spirng Security 认证的一个步骤,需要保存登录用户到回话中,而OAuth2AuthorizationCodeGrantFilter是一个独立功能的过滤器, 用于帮助完成AccessToken的获取, 两者都依赖于Spring Security对Oauth2功能支持的类。如:ClientRegistration,OAuth2AuthorizationCodeAuthenticationToken, OAuth2AuthorizationCodeAuthenticationProvider, OAuth2AuthorizedClientRepository等。
AuthorizationRequestRepository
在整个认证过程中存储OAuth2AuthorizationRequest。用于关联和验证Authorization Response。
默认实现是HttpSessionOAuth2AuthorizationRequestRepository,将OAuth2AuthorizationRequest保存于HttpSession中。
OAuth2AccessTokenResponseClient
授权码模式下AccessToken获取接口,默认实现是
DefaultAuthorizationCodeTokenResponseClient 使用RestOperations来交换授权码和AccessToken。提供了针对请求前预处理和响应定制处理扩展点来满足扩展。
- setRequestEntityConverter方法设置请求的前置处理器。默认实现是:OAuth2AuthorizationCodeGrantRequestEntityConverter:用于构建一个标准的OAuth2.0 Accesss Token 请求。
- SetRestOperation 方法可以定制RestOperation,对请求响应进行配置, 默认配置如下:
RestTemplate restTemplate = new RestTemplate( Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter())); restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); this.restOperations = restTemplate; |
DefaultRefreshTokenTokenResponseClient
实现Access Token 刷新流程,类似OAuth2AccessTokenResponseClient
JwtBearerOAuth2AuthorizedClientProvider
支持通过JWT获取AccessToken的客户端
项目实施-统一登录客户端
上面介绍了Oauth2.0服务和Spring Security的基本原理,从项目的角度需要实现的以Oauth的客户端的角色接入到公司的统一认证平台。
方式一 基于Spring Security的Oauth标准流程进行接入
基于Spring Security的Oauth标准流程进行接入。针对上文解析中涉及到的各个扩展点,如
- ClientRegistrationReporisoty配置存取接口: 通常实际项目中的统一登录相关配置是保存在数据库或配置中心上。
- OAuth2AccessTokenResponseClient: 获取accessToken的接口,项目的accesssToken获取接口可能是所谓非标准的流程。
- 实现特定的AuthenticationSuccessHandler或AuthenticationFailureHandler来对认证结果进行后续处理。
优点:Spring Security框架成熟, 对于OAUTH2.0的标准协议可以迅速接入,稳定性高,扩展性强,目前Spring Security是主流的安全框架, 对OAUTH2后续可能的新增特性的可以保证持续更新。
缺点: Spring Security的Oauth2.0流程设计上存在一定复杂度,设计较多接口和类,有一定门槛, 实际项目中通常仅仅是为了完成OAUTH2的流程,如果深度集成到其流程中,需要较高的成本。
方式二 基于Spring Security 的认证流程接入。
这是方式一的一种取舍, 当项目中使用了Spring Security来进行认证后,需要补充OAuth2来完成统一登录的情况下, 可以直接基于AbstractAuthenticationProcessingFilter 自己实现OAuth2.0流程。
优点: 架构简单,实现代码集中,避免了Spring Security OAuth2.0的学习成本。
缺点:需要重复开发
方式三 不基于Spring Security实现OAuth2.0流程
这个方式是针对没有使用Spring Security的项目,比如使用了Shiro,直接按照OAuth2.0的流程实现响应的Http相关接口。
优点:避免引入过多依赖, 保持架构简洁。
缺点: 需要独立开发。
总结: 推荐使用方式二或方式三的实现统一登录, 毕竟统一登录对于企业数字化项目来说并不是一个经常改变的项目,通常所以一次性的。 另外OAUTH2.0本身的交互流程并不复杂,实现的难度不高,相对于引入新的框架的学习成本更低。