【OAuth2.0】Spring Security OAuth2.0對於OAuth2.0協議中四種受權類型的實現

前言
OAuth2.0定義的幾種Authorization Grant

 Authorization Grant是資源全部者賦予第三方client的一份證書,第三方client能夠憑此證書獲取一個access token,後者能夠用來直接訪問資源全部者的某些受限資源,而不用知道資源全部者的用戶名密碼等信息。html

OAuth2.0中目前有四種受權類型:Authorization Code,implicit,resource owner password credentials,client credentialsjava

 

實現
Spring的具體實現(TokenGranter)

 上述幾種類型的受權類型中,Spring有對應的幾種TokenGranter,類繼承結構以下圖:spring


能夠看到類CompositeTokenGranter和AbstractTokenGranter直接implements接口TokenGranter。其中,緩存

前者實際類定義中維持了一個對全部具體TokenGranter的引用list,調用其grant方法時,會遍歷該list,調用每個引用的grant方法,直到有granter生成並返回OAuth2AccessToken或者list到最後。該TokenGranter用到的地方本身看代碼的過程當中,發如今TokenEndpoint中有對其的直接引用,實際結合對authorization-server的配置就能夠直到這個CompositeTokenGranter對其餘TokenGranter的引用list怎麼來的了:app

 

<oauth2:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
                                user-approval-handler-ref="oauthUserApprovalHandler"
                                user-approval-page="oauth_approval"
                                error-page="oauth_error">
       <!--後面這幾種受權方式應該都會整合進入CompositeTokenGranter的List<TokenGranter>屬性當中的-->
       <oauth2:authorization-code authorization-code-services-ref="authorizationCodeServices" />
       <oauth2:implicit/>
       <oauth2:refresh-token/>
       <oauth2:client-credentials/>
       <oauth2:password/>
   </oauth2:authorization-server>
            然後者AbstractTokenGranter則是個抽象類,有具體實現的TokenGranter會繼承該類

 

接口TokenGranter用於生成OAuth2AccessToken,接口就定義了一個方法:OAuth2AccessToken grant(String grantType, AuthorizationRequest authorizationRequest)。OAuth2AccessToken定義以下,其getValue()方法返回的便是返回客戶端的access_token。ide

 

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="http://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="http://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="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.3">Section 3.3</a>
	 */
	public static String SCOPE = "scope";

	/**
	 * The additionalInformation map is used by the token serializers to export any fields used by extensions of OAuth.
	 * @return a map from the field name in the serialized token to the value to be exported. The default serializers 
	 * make use of Jackson's automatic JSON mapping for Java objects (for the Token Endpoint flows) or implicitly call 
	 * .toString() on the "value" object (for the implicit flow) as part of the serialization process.
	 */
	Map<String, Object> getAdditionalInformation();

	Set<String> getScope();

	OAuth2RefreshToken getRefreshToken();

	String getTokenType();

	boolean isExpired();

	Date getExpiration();

	int getExpiresIn();

	String getValue();

}
  • AuthorizationCodeTokenGranter

咱們的項目使用的受權類型是Authorization Code,其他受權類型暫時徹底沒看的狀態。ui

結合上面說的,第三方client會先申請authorization code(這部分邏輯之後單獨說),獲得code以後,由第三方client向/auth/token發起獲取access token請求(調用的實際就是TokenEndpoint中的邏輯),相似:http://localhost:8080/oauth/token?client_id=id&client_secret=secret&grant_type=authorization_code&scope=read,write&redirect_uri=none&code=3iW8lAthis

 

@RequestMapping(value = "/oauth/token")
public class TokenEndpoint extends AbstractEndpoint {

	@RequestMapping
	public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal,
			@RequestParam(value = "grant_type", required = false) String grantType,
			@RequestParam Map<String, String> parameters) {

		//...
                //...
                //部分代碼省略 getTokenGranter()返回的是CompositeTokenGranter對象
		OAuth2AccessToken token = getTokenGranter().grant(grantType, authorizationRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + grantType);
		}

		return getResponse(token);

	}
}
 

 

而CompositeTokenGranter的實現以下:spa

 

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, AuthorizationRequest authorizationRequest) {
		for (TokenGranter granter : tokenGranters) {
			OAuth2AccessToken grant = granter.grant(grantType, authorizationRequest);
			if (grant!=null) {
				return grant;
			}
		}
		return null;
	}

}
 

 

因此,這樣,若是咱們上面的xml配置中,啓用了AuthorizaitonCode受權方式,那麼這裏就會使用到,並調用其grant方法獲取access token(grant方法實際是AbstractTokenGranter類中定義好的,子類只須要定義好其須要的幾個方法便可,好比getOAuth2Authentication())debug

in class AbstractTokenGranter:

 

public OAuth2AccessToken grant(String grantType, AuthorizationRequest authorizationRequest) {

		if (!this.grantType.equals(grantType)) {
			return null;
		}
		
		String clientId = authorizationRequest.getClientId();
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
		validateGrantType(grantType, client);
		
		logger.debug("Getting access token for: " + clientId);
		return getAccessToken(authorizationRequest);

	}

	protected OAuth2AccessToken getAccessToken(AuthorizationRequest authorizationRequest) {
		DefaultAuthorizationRequest outgoingRequest  = new DefaultAuthorizationRequest(authorizationRequest);
		outgoingRequest.setApproved(true);
		// FIXME: do we need to explicitly set approved flag here?
		return tokenServices.createAccessToken(getOAuth2Authentication(outgoingRequest));
	}
 in subclass AuthorizationCodeTokenGranter(會對client id和redirect uri等進行校驗,而且要注意的是,由於是使用code換取access token,因此這裏還須要另用掉的code失效):

 

 

@Override
	protected OAuth2Authentication getOAuth2Authentication(AuthorizationRequest authorizationRequest) {

		Map<String, String> parameters = authorizationRequest.getAuthorizationParameters();
		String authorizationCode = parameters.get("code");
		String redirectUri = parameters.get(AuthorizationRequest.REDIRECT_URI);

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

		AuthorizationRequestHolder storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);//讓code失效,若是使用DB的話,就是刪除該code對應的記錄,這裏DB刪除以後會把這條記錄同時返回
		if (storedAuth == null) {
			throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
		}

		AuthorizationRequest pendingAuthorizationRequest = storedAuth.getAuthenticationRequest();//反序列化,client申請code時是將code及其對應的AuthorizationRequest對象序列化以後一併存入Db的,因此這裏能夠反序列化出來使用
		// https://jira.springsource.org/browse/SECOAUTH-333
		// This might be null, if the authorization was done without the redirect_uri parameter
		String redirectUriApprovalParameter = pendingAuthorizationRequest.getAuthorizationParameters().get(
				AuthorizationRequest.REDIRECT_URI);

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

		String pendingClientId = pendingAuthorizationRequest.getClientId();
		String clientId = authorizationRequest.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>(storedAuth.getAuthenticationRequest()
				.getAuthorizationParameters());
		// Combine the parameters adding the new ones last so they override if there are any clashes
		combinedParameters.putAll(parameters);
		// Similarly scopes are not required in the token request, so we don't make a comparison here, just
		// enforce validity through the AuthorizationRequestFactory.
		DefaultAuthorizationRequest outgoingRequest = new DefaultAuthorizationRequest(pendingAuthorizationRequest);
		outgoingRequest.setAuthorizationParameters(combinedParameters);

		Authentication userAuth = storedAuth.getUserAuthentication();
		return new OAuth2Authentication(outgoingRequest, userAuth);

	}
 
以後剩餘的就是生成token的流程了。源碼裏面都寫的很清晰。
本身記住的就幾點:
  1. token的value是一個UUID值,而存儲進入DB的是對這個UUID值處理以後的值(相似MD5),因此想直接根據返回給client的value直接查詢DB是有點麻煩的==,=
  2. 貌似authorization id也是相似的生成邏輯....
  3. 在如今項目oauth相關的表存儲中,不少都用到了序列化將整個對象存儲爲blob類型的字段,還涉及到反序列化
  4. 單步調試的過程當中,發現,一次申請access token的請求中,屢次調用了根據clientId查詢DB的狀況,因此在實際使用中,考慮使用緩存。目前咱們本身對查詢client和查詢user信息加了緩存,token的緩存略麻煩,暫時沒加。

 

  •  其餘待續...
相關文章
相關標籤/搜索