【OAuth2.0】Spring Security OAuth2.0对于OAuth2.0协议中四种受权类型的实现
Authorization Grant是资源所有者赋予第三方client的一份证书,第三方client可以凭此证书获取一个access token,后者可以用来直接访问资源所有者的某些受限资源,而不用知道资源所有者的用户名密码等信息。
OAuth2.0中目前有四种授权类型:Authorization Code,implicit,resource owner password credentials,client credentials
上述几种类型的授权类型中,Spring有对应的几种TokenGranter,类继承结构如下图:
可以看到类CompositeTokenGranter和AbstractTokenGranter直接implements接口TokenGranter。其中,
前者实际类定义中维持了一个对所有具体TokenGranter的引用list,调用其grant方法时,会遍历该list,调用每一个引用的grant方法,直到有granter生成并返回OAuth2AccessToken或者list到最后。该TokenGranter用到的地方自己看代码的过程中,发现在TokenEndpoint中有对其的直接引用,实际结合对authorization-server的配置就可以直到这个CompositeTokenGranter对其他TokenGranter的引用list怎么来的了:
<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。
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,其余授权类型暂时完全没看的状态。
结合上面说的,第三方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=3iW8lA
@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的实现如下:
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())
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的value是一个UUID值,而存储进入DB的是对这个UUID值处理之后的值(类似MD5),所以想直接根据返回给client的value直接查询DB是有点麻烦的==,=
- 貌似authorization id也是类似的生成逻辑....
- 在现在项目oauth相关的表存储中,很多都用到了序列化将整个对象存储为blob类型的字段,还涉及到反序列化
- 单步调试的过程中,发现,一次申请access token的请求中,多次调用了根据clientId查询DB的情况,所以在实际使用中,考虑使用缓存。目前我们自己对查询client和查询user信息加了缓存,token的缓存略麻烦,暂时没加。
- 其他待续...