SpringSecurity OAuth2 获取Token端点TokenEndpoint、Token授权TokenGranter接口 详解

导读:本篇文章讲解 SpringSecurity OAuth2 获取Token端点TokenEndpoint、Token授权TokenGranter接口 详解,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1、前言

  在《授权服务器是如何实现授权的呢?》中,我们可以了解到服务端实现授权的流程,同时知道,当授权端点AuthorizationEndpoint生成授权码时,就会重定向到客户端的请求地址,这个时候,客户端就会拿着授权码再来授权服务器换取对应的Token,这篇内容,我们就详细分析如何使用授权码code换取Token的。在前面文章中,我们可以了解到客户端是通过“/oauth/token”来换取token的,该接口对应TokenEndpoint类的postAccessToken()方法,我们这篇文章就围绕获取token的TokenEndpoint类进行。

2、TokenEndpoint 获取Token的端点

  TokenEndpoint 是OAuth2规范中描述的令牌请求的端点,主要实现客户端获取token的能力,提供了”/oauth/token”接口,暴露给客户端用来获取Token。

  和授权端点AuthorizationEndpoint 类似,令牌请求端点TokenEndpoint 也继承自AbstractEndpoint抽象类,在《SpringSecurity OAuth2授权端点AuthorizationEndpoint、授权码AuthorizationCodeServices 详解》中,已经分析了AbstractEndpoint抽象类的实现,主要是初始化了TokenGranter、ClientDetailsService、OAuth2RequestFactory和WebResponseExceptionTranslator等对象,这里不再贴出代码进行分析了。
在这里插入图片描述

  和授权端点AuthorizationEndpoint相比,令牌请求端点TokenEndpoint更加简单一些,因为这里只提供了一个Post类型的”/oauth/token”token请求接口(GET类型的token请求接口忽略,实际是调用POST方式实现的,默认不开启),而授权端点AuthorizationEndpoint涉及到授权接口、授权同意接口等,在授权过程中还涉及到了用户交互操作。

“/oauth/token” 令牌请求

  令牌请求端点TokenEndpoint,提供了”/oauth/token”接口,暴露给客户端用来获取Token。默认只支持POST方法,可以通过allowedRequestMethods配置运行GET方法。

  该方法的调用发生在授权请求之后,跳转到业务界面之前,即需要访问授权的业务页面时,使用获取的授权码code,来换取对应的token,不过该步骤对前端的浏览器是不可见的,发生授权服务器和业务客户端之间的请求。

  ”/oauth/token”接口对应的postAccessToken()方法,实现的逻辑如下:

  1. 首先,判断请求中的principal参数是不是Authentication类型,该方法只处理Authentication类型的参数,不是该类型参数的直接抛出InsufficientAuthenticationException异常。
  2. 然后,通过getClientId()方法,从principal参数中获取client的信息(clientId)。首先判断,是否已经被授权,如果没有被授权,就直接抛出InsufficientAuthenticationException异常,如果已经授权就返回对应的clientId,其中OAuth2Authentication类型的参数时,获取clientId是通过OAuth2Request对象获取。
protected String getClientId(Principal principal) {
	Authentication client = (Authentication) principal;
	if (!client.isAuthenticated()) {
		throw new InsufficientAuthenticationException("The client is not authenticated.");
	}
	String clientId = client.getName();
	if (client instanceof OAuth2Authentication) {
		// Might be a client and user combined authentication
		clientId = ((OAuth2Authentication) client).getOAuth2Request().getClientId();
	}
	return clientId;
}
  1. 获取客户端信息(ClientDetails)。根据上一步获取到的clientId,通过ClientDetailsService获取对应ClientDetails信息。默认提供了两种ClientDetailsService对象的实现,也可以自定义进行实现。后续详细分析ClientDetailsService实现类。
  2. 创建TokenRequest对象。根据传递的参数parameters和获取到的客户端详细信息authenticatedClient,通过OAuth2RequestFactory,创建TokenRequest对象。其中,OAuth2RequestFactory默认使用DefaultOAuth2RequestFactory对象。
  3. 验证clientId。clientId不能为空,且请求中的client信息,要与存储在授权服务器端的客户端信息保持一致。
  4. 校验scope。通过OAuth2RequestValidator对象进行校验scope,默认实现DefaultOAuth2RequestValidator,通过对比请求中的scope和客户端authenticatedClient对象进行比较,进而实现判断。
  5. 判断grant type。不能为空,且在该模式下,不支持implicit(简单)认证模式。
  6. 设置scope。判断是否是刷新token或授权码验证,并根据结果设置对应的scope。
  7. 生成Token。到这里,就是真正产生Token的地方了。通过TokenGranter,来生成对应的Token对象。关于TokenGranter实现方式,后续将会详细介绍。
    10.返回对象。把生成的token,通过调用getResponse()方法进行返回。

 &esmp;关于”/oauth/token”接口对应的postAccessToken()方法的完整实现如下:

@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);
	ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

	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)));
	}
	OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
	if (token == null) {
		throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
	}
	return getResponse(token);
}

3、ClientDetailsService 客户端详细信息查询

  在前面的postAccessToken()方法中,用到了ClientDetailsService的loadClientByClientId()方法获取对应ClientDetails信息,这里我们着重看一下ClientDetailsService 实现方式。

  ClientDetailsService 客户端信息管理,提供了根据clientId查询客户端详细信息的方法,框架提供了InMemoryClientDetailsService和JdbcClientDetailsService两个实现类。
在这里插入图片描述

ClientDetailsService接口

  ClientDetailsService接口定义了一个查询客户端详细信息的接口,如下所示:

public interface ClientDetailsService {
  /**
   * 查询客户端详细信息
   */
  ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException;

}
InMemoryClientDetailsService实现类

  InMemoryClientDetailsService实现了ClientDetailsService接口,该实现类实现了客户端信息的内存存储,即存储在了定义的clientDetailsStore属性(Map<String, ClientDetails>类型)中,key对应clientId,value对应客户端详细信息ClientDetails。

  除了实现了接口中定义的loadClientByClientId()方法,还提供了一个设置客户端信息的方法(即为clientDetailsStore 属性赋值)setClientDetailsStore()。具体实现如下:

public class InMemoryClientDetailsService implements ClientDetailsService {

  private Map<String, ClientDetails> clientDetailsStore = new HashMap<String, ClientDetails>();

  public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
    ClientDetails details = clientDetailsStore.get(clientId);
    if (details == null) {
      throw new NoSuchClientException("No client with requested id: " + clientId);
    }
    return details;
  }

  public void setClientDetailsStore(Map<String, ? extends ClientDetails> clientDetailsStore) {
    this.clientDetailsStore = new HashMap<String, ClientDetails>(clientDetailsStore);
  }
}

  JdbcClientDetailsService实现类和InMemoryClientDetailsService接口类似,不过JdbcClientDetailsService不仅实现了ClientDetailsService 接口,还实现了ClientRegistrationService接口,即提供了客户端信息的注册能力。

  ClientRegistrationService接口定义,如下:

public interface ClientRegistrationService {
	//增加客户端信息
	void addClientDetails(ClientDetails clientDetails) throws ClientAlreadyExistsException;
	//修改客户端信息
	void updateClientDetails(ClientDetails clientDetails) throws NoSuchClientException;
	//更新客户端秘钥
	void updateClientSecret(String clientId, String secret) throws NoSuchClientException;
	//删除客户端信息
	void removeClientDetails(String clientId) throws NoSuchClientException;
	//查询客户端信息
	List<ClientDetails> listClientDetails();
}

4、TokenGranter 生成授权token

4.1、TokenGranter 层级结构

在这里插入图片描述
  其中,AuthorizationCodeTokenGranter 授权码模式、ClientCredentialsTokenGranter 客户端模式、ImplicitTokenGranter implicit 模式、RefreshTokenGranter 刷新 token 模式、ResourceOwnerPasswordTokenGranter 密码模式。组合代理类 CompositeTokenGranter。

  TokenGranter 接口有两个子类,其中AbstractTokenGranter 抽象类是 TokenGranter 接口的通用实现,其他真正实现TokenGranter 功能的类,都继承自AbstractTokenGranter 抽象类,而CompositeTokenGranter子类主要是为了组合使用其他TokenGranter实现类。

4.2、TokenGranter 接口
public interface TokenGranter {
	//生成Token
	OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);
}
4.3、AbstractTokenGranter 抽象类

  在AbstractTokenGranter抽象类中,定义了AuthorizationServerTokenServices、ClientDetailsService、OAuth2RequestFactory和grantType四个字段,并在提供了一个带四个参数的构造函数。

  grant()方法的实现逻辑:首先,验证grantType是否匹配,然后通过clientDetailsService对象获取客户端ClientDetails信息,验证客户端是否支持当前请求的grantType类型,最后再通过getAccessToken()方法获取OAuth2AccessToken对象。具体实现如下:

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);
}

  在getAccessToken()方法中,首先通过调用getOAuth2Authentication()方法,获取OAuth2Authentication对象,然后又通过tokenServices的createAccessToken()方法创建Token对象,具体实现如下:

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

  在getOAuth2Authentication()方法中,首先通过OAuth2RequestFactory对象创建storedOAuth2Request对象,然后根据该对象创建OAuth2Authentication实例并返回,具体实现如下:

protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
	OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
	return new OAuth2Authentication(storedOAuth2Request, null);
}

  在抽象类AbstractTokenGranter的子类中,就是通过重写grant()方法、getAccessToken()方法或getOAuth2Authentication()方法实现对应功能的。我们分别进行分析:

4.4、RefreshTokenGranter 刷新Token的实现

  RefreshTokenGranter子类,是通过重写getAccessToken()方法实现刷新Token功能的,在抽象类中定义的getAccessToken()方法是通过AuthorizationServerTokenServices的createAccessToken()方法创建对象的,而这里通过调用AuthorizationServerTokenServices的refreshAccessToken()方法实现刷新token的功能,关于AuthorizationServerTokenServices后续后专门详细分析,这里暂不展开,具体实现如下:

//RefreshTokenGranter.java
@Override
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
	String refreshToken = tokenRequest.getRequestParameters().get("refresh_token");
	return getTokenServices().refreshAccessToken(refreshToken, tokenRequest);
}
4.5、ClientCredentialsTokenGranter 客户端授权模式

  ClientCredentialsTokenGranter子类,是通过重写grant()方法实现客户端授权功能的,首先通过调用父类的grant()方法获取对应OAuth2AccessToken 对象,然后再根据allowRefresh参数设置refreshToken为空即可,说明一般客户端的认证不允许刷新token。

//ClientCredentialsTokenGranter.java
@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
	OAuth2AccessToken token = super.grant(grantType, tokenRequest);
	if (token != null) {
		DefaultOAuth2AccessToken norefresh = new DefaultOAuth2AccessToken(token);
		// The spec says that client credentials should not be allowed to get a refresh token
		if (!allowRefresh) {
			norefresh.setRefreshToken(null);
		}
		token = norefresh;
	}
	return token;
}
4.5、ResourceOwnerPasswordTokenGranter

  ResourceOwnerPasswordTokenGranter子类,是通过重写getOAuth2Authentication()方法实现资源授权功能的。

  在ResourceOwnerPasswordTokenGranter子类中,又增加了一个AuthenticationManager字段的定义,主要用来实现用户名密码的验证,并生成对应的Authentication对象,具体实现如下:

//ResourceOwnerPasswordTokenGranter.java
@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");

	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 storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);		
	return new OAuth2Authentication(storedOAuth2Request, userAuth);
}

  在ResourceOwnerPasswordTokenGranter重写的getOAuth2Authentication()方法中,首先获取用户名密码构建UsernamePasswordAuthenticationToken对象,然后通过AuthenticationManager进行校验,返回校验过的Authentication对象,否则就会抛出对应的异常,认证成功后,再通过OAuth2RequestFactory创建OAuth2Request对象,最后new一个OAuth2Authentication实例对象并返回,并在父类中根据该对象创建OAuth2AccessToken对象。即该过程中,首先完成了用户名密码的校验,然后才生成对应的token。

4.6、AuthorizationCodeTokenGranter

  AuthorizationCodeTokenGranter子类,和ResourceOwnerPasswordTokenGranter类一样,都是通过重写getOAuth2Authentication()方法实现对应功能的。但是在AuthorizationCodeTokenGranter类中,引入了AuthorizationCodeServices属性,通过调用consumeAuthorizationCode()方法,获取授权码对应的用户认证信息OAuth2Authentication,然后再根据认证信息获取存储的OAuth2Request对象,再获取其中的redirectUri和ClientId参数与调用传递参数对比校验,再创建新的OAuth2Request对象,并结合获取的Authentication对象,new一个OAuth2Authentication实例对象进行返回,并在父类中根据该对象创建OAuth2AccessToken对象。具体实现如下:

//AuthorizationCodeTokenGranter.java
@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();
	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)) {
		throw new InvalidClientException("Client ID mismatch");
	}
	Map<String, String> combinedParameters = new HashMap<String, String>(pendingOAuth2Request
			.getRequestParameters());
	combinedParameters.putAll(parameters);
	OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);
	Authentication userAuth = storedAuth.getUserAuthentication();
	return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);
}
4.7、ImplicitTokenGranter

  ImplicitTokenGranter子类,和AuthorizationCodeTokenGranter 、ResourceOwnerPasswordTokenGranter类一样,都是通过重写getOAuth2Authentication()方法实现对应功能的。不过在ImplicitTokenGranter重写的getOAuth2Authentication()方法中,不需要再做校验,直接获取SpringSecurity上下文中存储的用户认证信息即可,具体实现如下:

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

	Authentication userAuth = SecurityContextHolder.getContext().getAuthentication();
	if (userAuth==null || !userAuth.isAuthenticated()) {
		throw new InsufficientAuthenticationException("There is no currently logged in user");
	}
	Assert.state(clientToken instanceof ImplicitTokenRequest, "An ImplicitTokenRequest is required here. Caller needs to wrap the TokenRequest.");

	OAuth2Request requestForStorage = ((ImplicitTokenRequest)clientToken).getOAuth2Request();

	return new OAuth2Authentication(requestForStorage, userAuth);
}

  TokenGranter接口的CompositeTokenGranter实现类,这里不再详细信息,就是代理真的实现类使用,可以组合多种TokenGranter实现类,循环调用即可。

  在TokenGranter接口的几个实现类中,仔细回想一下,其实真正实现token生成的其实是AuthorizationServerTokenServices对象,在RefreshTokenGranter实现类中是通过调用refreshAccessToken()方法实现,而在AuthorizationCodeTokenGranter、ImplicitTokenGranter和ResourceOwnerPasswordTokenGranter三个子类中,是通过重写getOAuth2Authentication()方法,获取对应的认证信息-OAuth2Authentication对象,然后再使用获取到的认证信息,调用AuthorizationServerTokenServices对象的createAccessToken()方法来生成token对象,而ClientCredentialsTokenGranter实现类则是直接使用了抽象类中的定义方法,实际上也是通过AuthorizationServerTokenServices对象的createAccessToken()方法来生成token对象,所以归根结底,生成token的方法又落到了AuthorizationServerTokenServices对象上。后续,我们专门一篇博文分析AuthorizationServerTokenServices的实现。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68719.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!