SpringOauth2.0源码分析之获取access_token(四)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014730165/article/details/83374806

1.概述

前面三个章节叙述了用户名密码认证方式中客户端用户名密码认证细节。

  1. SpringOauth2.0源码分析之认证流程分析(一)
  2. SpringOauth2.0源码分析之ProviderManager(二)
  3. SpringOauth2.0源码分析之客户端认证(三)

本章节主要深入分析access_token的实现细节。整个流程实现细节如下:
在这里插入图片描述
整个流程中主要核心分为三大块:

  1. 用户的用户名密码认证
  2. 根据用户名,客户端信息,权限信息生成对应的Token

下面的分析根据这两大模块依次分析讲解。


2.用户的用户名密码认证

2.1 访问/oauth/token url 接口

该接口是实现生成AccessToken的入口。

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
	@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
		// 验证客户端是否认证成功
		if (!(principal instanceof Authentication)) {
			throw new InsufficientAuthenticationException(
					"There is no client authentication. Try adding an appropriate authentication filter.");
		}
		String clientId = getClientId(principal);
		// 根据ClientId查询当前客户端的详细信息
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
		// 根据客户端信息和请求参数,封装成TokenRequest对象
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
		// 再次校验当前客户端信息,防止有人修改造成不一致情况
		if (clientId != null && !clientId.equals("")) {
			// Only validate the client details if a client authenticated during this
			// request.
			if (!clientId.equals(tokenRequest.getClientId())) {
				// double check to make sure that the client ID in the token request is the same as that in the
				// authenticated client
				throw new InvalidClientException("Given client ID does not match authenticated client");
			}
		}
		if (authenticatedClient != null) {
			oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
		}
		if (!StringUtils.hasText(tokenRequest.getGrantType())) {
			throw new InvalidRequestException("Missing grant type");
		}
		if (tokenRequest.getGrantType().equals("implicit")) {
			throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
		}
		if (isAuthCodeRequest(parameters)) {
			// The scope was requested or determined during the authorization step
			if (!tokenRequest.getScope().isEmpty()) {
				logger.debug("Clearing scope of incoming token request");
				tokenRequest.setScope(Collections.<String> emptySet());
			}
		
		if (isRefreshTokenRequest(parameters)) {
			// A refresh token has its own default scopes, so we should ignore any added by the factory here.
			tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
		}
		// 获取AccessToken
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}
		return getResponse(token);
	}
}

从实现源码细节中可以看出,主要做了客户端校验,以及获取OAuth2AccessToken 的两件核心事件。在获取OAuth2AccessToken对象的过程中,首先需要做的是用户的用户名密码认证。

2.2 验证用户的用户名和密码

当前的认证模式为用户名密码认证方式,其Token的整体授权类的继承如下:
在这里插入图片描述
这里我们使用的是:ResourceOwnerPasswordTokenGranter 授权类。首先进行用户的用户名密码认证。其核心代码如下:

@Override
	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
		Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
		String username = parameters.get("username");
		String password = parameters.get("password");
		// Protect from downstream leaks of password
		parameters.remove("password");
		// 封装成UsernamePasswordAuthenticationToken对象,找到对应的认证方式。
		Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
		((AbstractAuthenticationToken) userAuth).setDetails(parameters);
		try {
		// 认证当前的用户是不是存在
			userAuth = authenticationManager.authenticate(userAuth);
		}
		catch (AccountStatusException ase) {
			//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
			throw new InvalidGrantException(ase.getMessage());
		}
		catch (BadCredentialsException e) {
			// If the username/password are wrong the spec says we should send 400/invalid grant
			throw new InvalidGrantException(e.getMessage());
		}
		// 如果当前用户不存在,即抛出异常
		if (userAuth == null || !userAuth.isAuthenticated()) {
			throw new InvalidGrantException("Could not authenticate user: " + username);
		}
		// 当前用户存在即封装成OAuth2Request对象
		OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);		
		// 返回封装好的OAuth2Authentication实例对象
		return new OAuth2Authentication(storedOAuth2Request, userAuth);
	}

至此用户的用户名密码认证已经完成。接下来即是根据用户信息生成Token。


3.生成OAuth2AccessToken对象

3.1 OAuth2AccessToken对象详解

这一步也是最关键的一步,即生成OAuth2AccessToken对象,里面封装了认证完成的所有信息,下面我们深入的去看下其源码。首先看下其类继承关系图:
在这里插入图片描述
通过类继承图可以发现,其默认实现是:DefaultOAuth2AccessToken。其核心源码是:

public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {
	private static final long serialVersionUID = 914967629530462926L;
// 生动的access_token
	private String value;
// 过期时间
	private Date expiration;
// 刷新token方式
	private OAuth2RefreshToken refreshToken;
// 当前权限
	private Set<String> scope;
// 额外的增强参数
	private Map<String, Object> additionalInformation = Collections.emptyMap();
//构造函数
	public DefaultOAuth2AccessToken(String value) {
		this.value = value;
	}

	@SuppressWarnings("unused")
	private DefaultOAuth2AccessToken() {
		this((String) null);
	}

 // 构造函数
	public DefaultOAuth2AccessToken(OAuth2AccessToken accessToken) {
		this(accessToken.getValue());
		setAdditionalInformation(accessToken.getAdditionalInformation());
		setRefreshToken(accessToken.getRefreshToken());
		setExpiration(accessToken.getExpiration());
		setScope(accessToken.getScope());
		setTokenType(accessToken.getTokenType());
	}
}
3.2.DefaultTokenServices 生成Token

DefaultTokenServices 是Token的默认生成类,通过分析DefaultTokenServices生成类源码,我们可以清晰的知道Token的生成方式。下面我们看下其核心源码实现。其中标注数字的如:1,2,3等注解,都会进一步解析。

// 首先该类加了注解,保证其事务的完整性
@Transactional
	public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
	// 1.查询当前Token是否已经存在于数据库中
		OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
		OAuth2RefreshToken refreshToken = null;
		// 如果Token已经存在,做一下的逻辑处理
		if (existingAccessToken != null) {
		// 如果当前Token 已经存在,且已经过期。
			if (existingAccessToken.isExpired()) {
			// 如果当前的RefreshToken不为null的情况下。移除当前RefreshToken
				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);
				}
				// 移除AccessToken
				tokenStore.removeAccessToken(existingAccessToken);
			}
			// 如果token没有过期,还是使用原来的Token,重新存储。为了防止有权限修改
			. {
				// Re-store the access token in case the authentication has changed
				tokenStore.storeAccessToken(existingAccessToken, authentication);
				return existingAccessToken;
			}
		}

		// Only create a new refresh token if there wasn't an existing one
		// associated with an expired access token.
		// Clients might be holding existing refresh tokens, so we re-use it in
		// the case that the old access token
		// expired.
		if (refreshToken == null) {
			refreshToken = createRefreshToken(authentication);
		}
		// But the refresh token itself might need to be re-issued if it has
		// expired.
		else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
			ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
			if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
				refreshToken = createRefreshToken(authentication);
			}
		}
		// 2.创建OAuth2AccessToken 实例
		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;
	}
3.3 查询当前Token的TokenId的生成方式

查询当前Token是否已经存在于数据库中,最核心的功能模块是,怎么获取TokenId。我们通过源码分析,了解其TokenId的生成方式,主要通过DefaultAuthenticationKeyGenerator 类进行实现的:

public class DefaultAuthenticationKeyGenerator implements AuthenticationKeyGenerator {

	private static final String CLIENT_ID = "client_id";
	private static final String SCOPE = "scope";
	private static final String USERNAME = "username";
	// 封装核心参数,到map集合中
	public String extractKey(OAuth2Authentication authentication) {
		Map<String, String> values = new LinkedHashMap<String, String>();
		OAuth2Request authorizationRequest = authentication.getOAuth2Request();
		if (!authentication.isClientOnly()) {
			values.put(USERNAME, authentication.getName());
		}
		values.put(CLIENT_ID, authorizationRequest.getClientId());
		if (authorizationRequest.getScope() != null) {
			values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
		}
		return generateKey(values);
	}

	protected String generateKey(Map<String, String> values) {
		MessageDigest digest;
		try {
			digest = MessageDigest.getInstance("MD5");
			// 将核心的参数,变成字符串,在通过MD5加密
			byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));
			return String.format("%032x", new BigInteger(1, bytes));
		} catch (NoSuchAlgorithmException nsae) {
			throw new IllegalStateException("MD5 algorithm not available.  Fatal (should be in the JDK).", nsae);
		} catch (UnsupportedEncodingException uee) {
			throw new IllegalStateException("UTF-8 encoding not available.  Fatal (should be in the JDK).", uee);
		}
	}
}

通过分析源码可以发现,其生成方式,通过封装核心参数。主要有:客户端名称,用户名称,用户具备的权限信息,通过MD5加密生成TokenID。通过这段源码分析,可以确定,一个客户端,可以产生多个access_token。只要其权限,用户名不同即可。

3.4 创建OAuth2AccessToken实例对象的具体实现

创建OAuth2AccessToken中,比较核心的模块是access_token的实现方式。下面我们通过源码分析access_token是如何产生的。其实现类是:DefaultTokenServices

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
// 默认生成access_token的方式是UUID
		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;
	}

通过源码分析,我们得出,其默认生成access_token的方式是:UUID.randomUUID().toString()。


4.结语

通过生成AccessToken对象的核心源码分析。我们清楚的知道Token的TokenId是生成方式,以及access_token的生成方式。理解了整个用户名密码认证的流程。下一章节,将深入的分析Token的存储源码。敬请期待…

展开阅读全文

没有更多推荐了,返回首页