Spring Security Oauth2(二)

前言

上一篇我们是用了password模式来进行授权认证,获取token,接下来我们来看看其中关键的类是怎么获取token,分发token的。

ClientCredentialsTokenEndpointFilter

首先外部通过请求/oauth/token来获取token,当请求进来之前就会通过一个ClientCredentialsTokenEndpointFilter的过滤器,关键方如下:

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException, IOException, ServletException {
	//省略......
   
   // 从请求中获取客户端id和客户端凭证
   String clientId = request.getParameter("client_id");
   String clientSecret = request.getParameter("client_secret");

	//省略......
    
   clientId = clientId.trim();
   
   // 通过客户端Id和客户端凭证构建一个令牌
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
         clientSecret);
	// 把构建好的令牌对象交给AuthenticationManager去进行身份认证
   return this.getAuthenticationManager().authenticate(authRequest);
}

从代码上可以很清晰的看到,首先从请求获取到client_id,client_secret,组装成UsernamePasswordAuthenticationToken作为身份标识,然后去获取到getAuthenticationManager交给它的实现类ProviderManager去进行身份认证。

顶级身份管理者AuthenticationManager

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
	//省略......
   result = provider.authenticate(authentication);
	//省略......
}

从debug可以发现关键代码就是这一行,身份认证交给provider去做,至于这个provider是什么呢,我们接着往下看。

AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
	//省略......
   user = retrieveUser(username,
        (UsernamePasswordAuthenticationToken) authentication);
   //省略......

   return createSuccessAuthentication(principalToReturn, authentication, user);
}

从字面意思可以猜测检索用户行为,因为上面和下面都是一些验证的方法,这里就直接省略了,我们继续往里面走,

DaoAuthenticationProvider

进入上诉的方法,就会由DaoAuthenticationProvider实现类去做

protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   prepareTimingAttackProtection();
   try {
      UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
      if (loadedUser == null) {
         throw new InternalAuthenticationServiceException(
               "UserDetailsService returned null, which is an interface contract violation");
      }
      return loadedUser;
   }
   catch (UsernameNotFoundException ex) {
      mitigateAgainstTimingAttack(authentication);
      throw ex;
   }
   catch (InternalAuthenticationServiceException ex) {
      throw ex;
   }
   catch (Exception ex) {
      throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
   }
}

这里从上面可以看到去加载了我们重写的的实现类

image-20211202173744532

加载完这些信息后,身份信息就已经得到了AuthenticationManager,接下来就要到TokenEndpoint。

image-20211202174602114

TokenEndpoint

我们先来看看官方的解释,上面两个可以理解为前置校验和封装,这个类但从名字来看,就是和我们的授权有关,

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
	//省略......
   // 
   String clientId = getClientId(principal);
   ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
	//省略......
   TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
	//省略......
   OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
   if (token == null) {
      throw new UnsupportedGrantTypeException("Unsupported grant type");
   }

   return getResponse(token);

}
  1. 首先是去加载了我们重写的客户端实现类,具体的可以参照我上篇文章提供的示例代码

  2. 然后进入到createTokenRequest可以发现核心代码是做了

TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);

简单来说就是通过请求参数,范围,授权模式来封装了一个tokenRequest对象

  1. 将token对象交给TokenGranter来进行颁发token

我们来看一下token的组成:

public interface OAuth2AccessToken {

   public static String BEARER_TYPE = "Bearer";

   public static String OAUTH2_TYPE = "OAuth2";

   /**
    * The access token issued by the authorization server. This value is REQUIRED.
    */
   public static String ACCESS_TOKEN = "access_token";

   /**
    * The type of the token issued as described in <a
    * href="https://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-7.1">Section 7.1</a>. Value is case insensitive.
    * This value is REQUIRED.
    */
   public static String TOKEN_TYPE = "token_type";

   /**
    * The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will
    * expire in one hour from the time the response was generated. This value is OPTIONAL.
    */
   public static String EXPIRES_IN = "expires_in";

   /**
    * The refresh token which can be used to obtain new access tokens using the same authorization grant as described
    * in <a href="https://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-6">Section 6</a>. This value is OPTIONAL.
    */
   public static String REFRESH_TOKEN = "refresh_token";

   /**
    * The scope of the access token as described by <a
    * href="https://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.3">Section 3.3</a>
    */
   public static String SCOPE = "scope";
    
   //省略......

}
public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {

   private static final long serialVersionUID = 914967629530462926L;

   private String value;

   private Date expiration;

   private String tokenType = BEARER_TYPE.toLowerCase();

   private OAuth2RefreshToken refreshToken;

   private Set<String> scope;

   private Map<String, Object> additionalInformation = Collections.emptyMap();
   
   	//省略......
}

上面两个原石类就是我们请求/oauth/token的请求成功之后的对象,至于怎么颁发的呢,别急,继续往下走。

TokenGranter(掌握)

image-20211202181258555

TokenGranter的设计思路是使用CompositeTokenGranter管理一个List列表,每一种grantType对应一个具体的真正授权者,在debug过程中可以发现CompositeTokenGranter 内部就是在循环调用五种TokenGranter实现类的grant方法,而granter内部则是通过grantType来区分是否是各自的授权类型。

public class CompositeTokenGranter implements TokenGranter {

   private final List<TokenGranter> tokenGranters;

   public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
      this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
   }
   
   public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
      for (TokenGranter granter : tokenGranters) {
         OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
         if (grant!=null) {
            return grant;
         }
      }
      return null;
   }
   
   public void addTokenGranter(TokenGranter tokenGranter) {
      if (tokenGranter == null) {
         throw new IllegalArgumentException("Token granter is null");
      }
      tokenGranters.add(tokenGranter);
   }

}

五种类型分别是:

ResourceOwnerPasswordTokenGranter ==> password密码模式
AuthorizationCodeTokenGranter ==> authorization_code授权码模式
ClientCredentialsTokenGranter ==> client_credentials客户端模式
ImplicitTokenGranter ==> implicit简化模式
RefreshTokenGranter ==>refresh_token 刷新token专用

我们回到AbstractTokenGranter的方法,

public abstract class AbstractTokenGranter implements TokenGranter {
   //省略......
   public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

      if (!this.grantType.equals(grantType)) {
         return null;
      }
      
      String clientId = tokenRequest.getClientId();
       // 获取客户端信息
      ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
      validateGrantType(grantType, client);

      if (logger.isDebugEnabled()) {
         logger.debug("Getting access token for: " + clientId);
      }
		
      return getAccessToken(client, tokenRequest);

   }

   protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
      return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
   }
	//省略......
}

这上诉颁发的方法来看,我们可以看到,在颁发的时候首先加载了我们重写的客户端详情类clientDetailsService.loadClientByClientId(clientId);之后再通过getAccessToken里面的tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));来创建

我们debug往里面走可以发现,来到了DefaultService来进行创建,代码如下

@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
   // 检索根据提供的身份验证密钥存储的访问令牌(如果它存在的话)。
   OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
   OAuth2RefreshToken refreshToken = null;
   if (existingAccessToken != null) {
      if (existingAccessToken.isExpired()) {
         if (existingAccessToken.getRefreshToken() != null) {
            refreshToken = existingAccessToken.getRefreshToken();
            // The token store could remove the refresh token when the
            // access token is removed, but we want to
            // be sure...
            tokenStore.removeRefreshToken(refreshToken);
         }
         tokenStore.removeAccessToken(existingAccessToken);
      }
      else {
         // 重新存储访问令牌,以防身份验证发生更改
         tokenStore.storeAccessToken(existingAccessToken, authentication);
         return existingAccessToken;
      }
   }


	//如果没有与过期访问令牌关联的现有令牌,则只创建新的刷新令牌。客户端可能持有现有的刷新令牌,所以我们在旧访问令牌过期的情况下重用它。
   if (refreshToken == null) {
      refreshToken = createRefreshToken(authentication);
   }
   // 但是,如果刷新令牌已经过期,则可能需要重新发布它。
   else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
      ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
      if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
         refreshToken = createRefreshToken(authentication);
      }
   }

   OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
   tokenStore.storeAccessToken(accessToken, authentication);
   // In case it was modified
   refreshToken = accessToken.getRefreshToken();
   if (refreshToken != null) {
      tokenStore.storeRefreshToken(refreshToken, authentication);
   }
   return accessToken;

}

上诉的核心代码可以发现,进来先根据身份验证的令牌先去查询是否存在token,没有在进行createAccessToken(authentication, refreshToken);进行token的创建

进到createAccessToken()方法发现

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
   DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
   int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
   if (validitySeconds > 0) {
      token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
   }
   token.setRefreshToken(refreshToken);
   token.setScope(authentication.getOAuth2Request().getScope());

   return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}

原来这个token是UUID生成的,天下大白!原来如此,refreshToken也是uuid生成的,这里不再过多逼逼

官方解释:

Base implementation for token services using random UUID values for the access token and refresh token values. The main extension point for customizations is the {@link TokenEnhancer} which will be called after the access and refresh tokens have been generated but before they are stored.

令牌服务的基本实现使用访问令牌和刷新令牌值的随机UUID值。自定义的主要扩展点是{@link TokenEnhancer},它将在生成访问和刷新令牌之后、但在它们被存储之前被调用。

TokenService

public interface AuthorizationServerTokenServices {

   /**
    * Create an access token associated with the specified credentials.
    * 创建与指定凭证关联的访问令牌。
    * @param authentication The credentials associated with the access token.
    * @return The access token.
    * @throws AuthenticationException If the credentials are inadequate.
    */
   OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;

   /**
    * Refresh an access token. The authorization request should be used for 2 things (at least): to validate that the
    * client id of the original access token is the same as the one requesting the refresh, and to narrow the scopes
    * (if provided).
    * 
    * @param refreshToken The details about the refresh token.
    * @param tokenRequest The incoming token request.
    * @return The (new) access token.
    * @throws AuthenticationException If the refresh token is invalid or expired.
    */
   OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
         throws AuthenticationException;

   /**
    * Retrieve an access token stored against the provided authentication key, if it exists.
    * 
    * @param authentication the authentication key for the access token
    * 
    * @return the access token or null if there was none
    */
   OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

}

我们可以看到AuthorizationServerTokenServices,它集中了token的实现方法,比如创建token,刷新token,获取token等。

Finally

我们了解到了创建从请求/oauth/token到创建token的密码模式的整个流程,至于其他模式尽不相同,但功能都是一样的,只是会有不同的实现类去实现。

最后我是ikcross,附上一句座右铭,你所过的每一个平凡,可能都是一个个连续的奇迹

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值