统一身份认证服务
统一身份认证服务系统:以统一身份认证服务为核心,用户登录统一身份认证服务后,即可以使用所有支持统一身份认证服务的管理应用系统。
统一认证服务的提供方在项目实施中通常由公司平台层面提供统一平台,作为业务系统的任务是需要通过OAUTH2以调用方的方式接入平台。
开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。由统一认证平台负责管理用户名和密码,统一认证作为唯一的登录入口,就是单点登录SSO了。目前主要是OAUTH2.0版本。
(1)Third-party application:第三方应用程序(client),资源的请求方。
(2)HTTP service:HTTP服务提供商,对外提供受保护资源服务。
(3)Resource Owner:资源所有者。
(4)User Agent:用户代理,通常指浏览器。
(5)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
授权流程:
OAuth2.0的授权模式包括: 授权码模式,简化模式,密码模式和客户端模式。线面介绍一下授权码模式。
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
流程如下:
其中请求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架构建立在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的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); } |
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具体实现如下:
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 是AbstractAuthenticationProcessingFilter的实现类,处理授权码模式下的授权码返回的响应,生成OAuth2LoginAuthenticationToken委托给AuthenticationManager进行登录验证。
详细流程如下:
OAuth2AuthorizationCodeGrantFilter 是 OncePerRequestFilter的子类,是以独立功能形式的过滤器存在的,用于获取OAuth2.0授权码,处理OAuth2.0的授权响应。
授权响应的处理如下:
OAuth2AuthorizationCodeAuthenticationToken并委托给AuthenticationManager进行认证。
OAuth2LoginAuthenticationFilter 和 OAuth2AuthorizationCodeGrantFilter区别在前者是针对登录,是 Spirng Security 认证的一个步骤,需要保存登录用户到回话中,而OAuth2AuthorizationCodeGrantFilter是一个独立功能的过滤器, 用于帮助完成AccessToken的获取, 两者都依赖于Spring Security对Oauth2功能支持的类。如:ClientRegistration,OAuth2AuthorizationCodeAuthenticationToken, OAuth2AuthorizationCodeAuthenticationProvider, OAuth2AuthorizedClientRepository等。
在整个认证过程中存储OAuth2AuthorizationRequest。用于关联和验证Authorization Response。
默认实现是HttpSessionOAuth2AuthorizationRequestRepository,将OAuth2AuthorizationRequest保存于HttpSession中。
授权码模式下AccessToken获取接口,默认实现是
DefaultAuthorizationCodeTokenResponseClient 使用RestOperations来交换授权码和AccessToken。提供了针对请求前预处理和响应定制处理扩展点来满足扩展。
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框架成熟, 对于OAUTH2.0的标准协议可以迅速接入,稳定性高,扩展性强,目前Spring Security是主流的安全框架, 对OAUTH2后续可能的新增特性的可以保证持续更新。
缺点: Spring Security的Oauth2.0流程设计上存在一定复杂度,设计较多接口和类,有一定门槛, 实际项目中通常仅仅是为了完成OAUTH2的流程,如果深度集成到其流程中,需要较高的成本。
这是方式一的一种取舍, 当项目中使用了Spring Security来进行认证后,需要补充OAuth2来完成统一登录的情况下, 可以直接基于AbstractAuthenticationProcessingFilter 自己实现OAuth2.0流程。
优点: 架构简单,实现代码集中,避免了Spring Security OAuth2.0的学习成本。
缺点:需要重复开发
这个方式是针对没有使用Spring Security的项目,比如使用了Shiro,直接按照OAuth2.0的流程实现响应的Http相关接口。
优点:避免引入过多依赖, 保持架构简洁。
缺点: 需要独立开发。
总结: 推荐使用方式二或方式三的实现统一登录, 毕竟统一登录对于企业数字化项目来说并不是一个经常改变的项目,通常所以一次性的。 另外OAUTH2.0本身的交互流程并不复杂,实现的难度不高,相对于引入新的框架的学习成本更低。