当前位置:   article > 正文

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

tokenendpoint

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

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

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

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

ClientDetailsService接口

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

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

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
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);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

  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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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);
}
  • 1
  • 2
  • 3
  • 4
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

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

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

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

  在抽象类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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

  在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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/84086?site
推荐阅读
相关标签
  

闽ICP备14008679号