Spring Security OAuth 认证流程浅析:授权码模式

这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

上一篇(Spring Security OAuth 认证流程浅析:密码模式 ),简单分析了 Spring Security OAuth 密码模式授权流程的源码,这篇来试着分析 OAuth 中最具代表性的授权码模式。

由于内容的连贯性,建议读之前,先阅读上一篇分析密码模式的文章(Spring Security OAuth 认证流程浅析:密码模式 )。同时,阅读以下内容需要你了解 OAuth 的原理,可以阅读这篇文章回顾一下。

授权码模式的流程

先简单回顾一下授权码模式的流程。看下图:

  • A 步骤:用户访问客户端,客户端会将前者重定向到认证服务器。
  • B 步骤:认证服务器返回认证和权限确认的页面,用户选择是否给予客户端授权。
  • C 步骤:当用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码(authorization_code)。
  • D 步骤:客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
  • E 步骤:认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

请求授权码(authorization code)

在 Spring Security OAuth 中,获取授权码的方式如下:

GET HOST:PORT/oauth/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{redirect_uri}}&scope={{scope}}&state={{state}}
复制代码

我们可以在 AuthorizationEndpoint 类中,找到处理这个请求的代码。

这个方法的代码比较长,你可以自行查看。

第一次访问这个请求的时候,会做这么几件事儿:

  • 首先,校验 response_type 参数的值,只能是 code 或者 token。使用授权码模式的时候,给的参数值是 code,token 是在简易模式时使用的,这里不考虑。
  • 然后,验证 client_id 参数是不是提供了。
  • 之后,会判断用户是不是已经认证,因为我们是第一次请求,因此会跑出异常。

这里抛出的异常是 InsufficientAuthenticationException ,它的父类是 AuthenticationException ,异常被 ExceptionTranslationFilter 捕获后,会将此请求跳转到用户登录和授权的页面。

用户登录授权

当用户登录并且确认授权后,根据 Spring Security 的流程,会跳转到登录前请求的地址,因此会再次请求获取授权吗的地址。

这次请求后,用户已经被认证过,会进入之后的逻辑,进入请求时 client_id 对应的客户端的重定向地址。可参考下面的代码片段:

if ( authorizationRequest.isApproved ()) {
 if ( responseTypes.contains ( "token" )) {
 return getImplicitGrantResponse ( authorizationRequest ) ;
   }
 if ( responseTypes.contains ( "code" )) {
 return new ModelAndView ( getAuthorizationCodeResponse ( authorizationRequest,
            ( Authentication ) principal )) ;
   }
 }
复制代码

在重定向到这个地址的时候,会包含授权服务器生成的授权码,这个请求会发送到客户端程序的后端服务,这样,客户端会接收到这个授权码。

至此,客户端在不接触用户名和密码的情况下,获取到了一个合法的授权码。

请求访问令牌(access token)

客户端在得到授权码以后,下一步就是请求访问令牌,请求方式如下:

POST HOST:PORT/oauth/token
Authorization: Basic <Base64("clientId:clientSecret")>
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code={{authorization_code}}&redirect_uri={{redirect_uri}}&client_id={{client_id}}&scope={{scope}}&state={{state}}
复制代码

请求的地址是 /oauth/token 与密码模式请求的地址相同,具体的处理逻辑可以参考上一篇分析密码模式的文章(Spring Security OAuth 认证流程浅析:密码模式 ),这里关键的一行代码是:

 OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
复制代码

因此,之后的逻辑是由 AuthorizationCodeTokenGranter 类型来处理的。这里掠过与上一篇文章中重复的内容,直接看 getOAuth2Authentication 方法的内容。我把代码贴出来:

@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

   Map<String, String> parameters = tokenRequest.getRequestParameters();
   String authorizationCode = parameters.get("code");
   String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);

   if (authorizationCode == null) {
      throw new InvalidRequestException("An authorization code must be supplied.");
   }

   OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
   if (storedAuth == null) {
      throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
   }

   OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
   // https://jira.springsource.org/browse/SECOAUTH-333
   // This might be null, if the authorization was done without the redirect_uri parameter
   String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(
         OAuth2Utils.REDIRECT_URI);

   if ((redirectUri != null || redirectUriApprovalParameter != null)
         && !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) {
      throw new RedirectMismatchException("Redirect URI mismatch.");
   }

   String pendingClientId = pendingOAuth2Request.getClientId();
   String clientId = tokenRequest.getClientId();
   if (clientId != null && !clientId.equals(pendingClientId)) {
      // just a sanity check.
      throw new InvalidClientException("Client ID mismatch");
   }

   // Secret is not required in the authorization request, so it won't be available
   // in the pendingAuthorizationRequest. We do want to check that a secret is provided
   // in the token request, but that happens elsewhere.

   Map<String, String> combinedParameters = new HashMap<String, String>(pendingOAuth2Request
         .getRequestParameters());
   // Combine the parameters adding the new ones last so they override if there are any clashes
   combinedParameters.putAll(parameters);
   
   // Make a new stored request with the combined parameters
   OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);
   
   Authentication userAuth = storedAuth.getUserAuthentication();
   
   return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);

}
复制代码

这段代码主要做了这么几件事:

  1. 从请求当中获取授权码(authorizationCode)和重定向地址(redirectUri)。
  2. 通过授权码获取 OAuth2Authentication 对象。
  3. OAuth2Authentication 中获取 OAuth2Request,并校验授权码和重定向地址的合法性。
  4. 创建新的 OAuth2Request 并创建新的 OAuth2Authentication 对象。

这个方法由父类中的方法调用,最终的结果,会返回到 tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)) 这行代码,生成最终的 Token 并返回给客户端。

猜你喜欢

转载自juejin.im/post/7055593575299416077