Spring Security OAuth核心类图解析
关于Oauth2是什么以及Oauth2的四种授权模式请移步Oauth2官网。
下面简单介绍一下关于Spring Security OAuth基本的原理。这也是理解pig及其pigx的第一步。
下面这张图涉及到了Spring OAuth的一些核心类和接口。
上图蓝色的方块代表执行过程中调用的具体的类,绿色的方块代表整个执行流程中调用的类,绿色的括号中代表的是该接口调用的具体的实现类。
整个流程的入口点是在TokenEndpoint,由它来处理获取令牌的请求,获取令牌的请求默认是**/oauth/token**这个路径。
1.当TokenEndpoint收到请求时,它首先会调用ClientDetailsService,ClientDetaisService从名字上看就很可以知道是一个类似于UserDetailsService的接口,只不过UserDetailsService读取的是用户的信息,而ClientDetailsService读取的是第三方应用的信息。
2.登录请求头中带上Client的信息,而这个类就可以做到根据ClientId读取相应的配置信息。而ClientDetailsSevice读取到的信息都会封装到ClientDetails这个对象中。
3.同时,TokenEndpoint还会创建一个TokenRequests的对象,这个对象中封装了除了第三方应用以外的其他信息。比如说grant_type,scope,username,password(限密码模式)等等信息,而这些信息都是封装在TokenRequests里面的。同时,ClientDetails也会被放到TokenRequests中,因为第三方应用的信息也是令牌请求的一部分。
4.之后利用TokenRequests去调用一个叫做TokenGranter的令牌授权者的接口,这个接口其实是对四种不同的授权模式进行的一个封装。在这个接口里,它会根据请求传递过来的grant_type去挑一个具体的实现来执行令牌生成的逻辑。
5.不论采用哪种方式进行令牌的生成,在这个生成的过程中都会产生两个对象,一个是OAuth2Request,这个对象实际上是之前的ClientDetails和TokenRequests这两个对象的一个整合。另一个Authorization封装的实际上是当前授权用户的一些信息,也就是谁在进行授权行为,Authorization里封装的就是谁的信息。这里的用户信息是通过UserDetailsService进行读取的。
6.OAuth2Request和Authorization这两个对象组合起来,会形成一个OAuth2Authorization对象,而这个最终产生的对象它的里面就包含了当前是哪个第三方应用在请求哪个用户以哪种授权模式(包括授权过程中的一些其他参数)进行授权,也就是这个对象会汇总之前的几个对象的信息都会封装到OAuth2Authorization这个对象中。
7.然后这个对象会传递到一个叫做AuthorizationServerTokenServices的接口的实现类,它拿到OAuth2Authorization中所有的信息之后最终会生成一个OAuth2的令牌OAuth2AccessToken。
Spring Security OAuth的令牌生成过程
下面的是一个标准的POST请求并且在URL中携带参数的请求,但是这个请求不符合我们这边测试的要求,原因看下面的注意事项。
curl -H "Authorization:Basic dGVzdDp0ZXN0" -X POST http://localhost:8000/auth/oauth/token?username=admin&password=123456&grant_type=password&scope=server
回车以后我们可以看到首先会经过网关的密码解密过滤器,并且参数经过我们的一通改造之后已经可以获取到正确的值了。
经过上面的一通操作,我们已经拿到了获取token的一些必要的请求了。clientId,clientSecret,grant_type,usename,password,scope,终于可以带着我们的参数深入源码啦!
这里结合上文提到的核心类图来看效果更好
上文提过,OAuth2.0的认证的入口点位于TokenEndPoint。我们也可以看到,代码确实已经进来了。
我们可以看到这个类上有一个@RequestMapping
注解,它来处理/oauth/token
的POST请求。
进来之后的第一步,就是在代码的95行,获取请求头中的clientId。
然后在96行调用
getClientDetailsService().loadClientByClientId(clientId)
方法获取整个第三方应用的详细配置。
具体的参数的意义可以看spring-oauth-server 数据库表说明
在拿到客户端的信息之后在代码的98行通过传递进来的参数和查询出来的第三方应用信息构建TokenRequest。
创建TokenRequest的代码很简单,如下:
public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) {
String clientId = requestParameters.get(OAuth2Utils.CLIENT_ID);
if (clientId == null) {
// if the clientId wasn't passed in in the map, we add pull it from the authenticated client object
clientId = authenticatedClient.getClientId();
}
else {
// otherwise, make sure that they match
if (!clientId.equals(authenticatedClient.getClientId())) {
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
String grantType = requestParameters.get(OAuth2Utils.GRANT_TYPE);
Set<String> scopes = extractScopes(requestParameters, clientId);
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
return tokenRequest;
}
所以其实它就干了一件事,校验传递进来clientId和查询出来的clientId,如果匹配的话,就根据之前传递进来的clientId和和查询出来的第三方应用构建TokenRequest。
然后我们就拿到TokenRequest了,后面的代码很简单了:
无非就是对下面这些参数的校验:
clientId:是否有值,值是否和查询结果匹配
scope:请求的一些授权内容,所请求的授权必须是第三方应用可以发送的授权集合的子集,否则无法通过校验)
grant_type:必须显式指定按照哪种授权模式获取令牌
判断传递的授权模式是否是简化模式,如果是简化模式也会抛异常。因为简化模式其实是对授权码模式的一种简化:在用户的第一步的授权行为的时候就直接返回令牌,所以是不会有调用请求令牌服务的机会的
判断是不是授权码模式,因为授权码模式包含两个步骤,在授权码模式中发出的令牌中拥有的权限不是由发令牌的请求决定的,而是在发令牌之前的授权的请求里就已经决定好了。因此它会对请求过来的scope进行置空操作,然后根据之前发出去的授权码里的权限重新设置你的scope,因此它根本不会使用请求令牌的这个请求中携带的scope参数。
之后判断是不是刷新令牌的请求,应为刷新令牌的请求有自己的scope,所以也会进行重新设置scope的操作。
经过一系列的校验之后,最终TokenRequest会在132行传递给TokenGranter,然后由granter产生最终的accessToken。之后直接将accessToken写入响应里就可以了。
TokenGranter中总共封装了四种授权模式加一个刷新令牌的操作,我们看看其中的一些细节。
CompositeTokenGranter中有一个集合,这个集合里封装着的就是五个会产生令牌的操作。
它会对遍历这五种情况,并根据之前请求中携带的grant_type在五种情况中挑一种进行最终的accessToken的生成。
然后我们看这个代码的第38行的具体的grant
方法。
首先在org.springframework.security.oauth2.provider.token.AbstractTokenGranter中判断当前携带的授权类型和这个类所支持的授权类型是否匹配,如果不匹配就返回空值,如果匹配的话就进行令牌的生成操作。
59到第63行是重新获取一下clientId和客户端信息跟授权类型再做一个校验,67行的getAccessToken
方法会产生最终的一个令牌。
这个方法也非常简单:
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
它实际上就是对tokenServices的一个调用,而tokenSerives其实就是从37行我们可以看到其实就是AuthorizationServerTokenServices。这个类要想创建accessToken需要一个OAuth2Authentication对象,所以createAccessToken中包含了一个方法getOAuth2Authentication
。
这个方法不同的授权模式会有不同的实现。
在Spring Security OAuth核心类图解析中我们已经知道最终产生的Oauth2Authorization包含两部分信息,一部分是请求中的一些信息,另一部分是根据请求获取的授权用户的信息。而在不同的授权模式下获取授权用户的信息的方式是不同的,比如说pigx所使用的密码模式就是使用请求中携带的用户名和密码来获取当前授权用户中的授权信息,而在授权码模式的两个步骤中是根据第一步发出授权码的同时会记录相关用户的信息,之后对第二步进行授权的时候根据第三方应用请求过来的授权码再读取该授权码对应的用户信息。所以getOAuth2Authentication对于不同的授权类型有不同的实现。
我们以pigx所使用的密码模式继续下面的流程。密码模式对应的是org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter
而这个方法我们可以看到它其实就是根据所请求的用户名和密码去创建UsernamePasswordAuthenticationToken,然后传递给authenticationManager做认证,在这个认证过程中它会去调用com.pig4cloud.pigx.common.security.service.PigxUserDetailsServiceImpl
的loadUserByUsername
方法,根据用户名和密码去读取用户的信息,之后我们其实就已经拿到Authorization的信息,而Oauth2Request根据第85行我们可以知道是根据传进来的第三方应用详情和tokenRequest产生出来的,而86行的OAuth2Authentication
也是由Oauth2Request
和Authorization
这两个对象拼接起来的。而拼接的方式就是调用 org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory
的createOAuth2Request
方法。
public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) {
return tokenRequest.createOAuth2Request(client);
}
这个方法最终会创建一个由clientDetails和tokenRequest组合而成的OAuth2Request。
拿到OAuth2Request
就可以去生成OAuth2Authentication
了。
而OAuth2Authentication
就是org.springframework.security.oauth2.provider.token.AbstractTokenGranter
第71到73行最终传递进去生成accessToken的对象。
而OAuth2Authentications生成成功之后进行返回的话就可以执行AuthorizationServerTokenServices
的createAccessToken
方法,而一旦这个access token生成成功并写入响应进行返回那么整个流程也就结束了,最终我们就拿到了想要的访问令牌。
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
具体创建accessToken的代码,我们需要仔细读一读org.springframework.security.oauth2.provider.token.DefaultTokenServices
的createAccessToken
方法。
首先这个类一进来就会尝试在tokenStore中获取accessToken,因为同一个用户只要令牌没过期那么再次请求令牌的时候会把之前发送的令牌再次发还。因此一开始就会找当前用户已经存在的令牌。
如果已经发送的令牌不为空,那么会在87行判断当前的令牌是否已经过期,如果令牌过期了,那么就会在tokenStore里把accessToken和refreshToken一起删掉,如果令牌没过期,那么就把这个没过期的令牌重新再存一下。因为可能用户是使用另外的方式来访问令牌的,比如说一开始用授权码模式,后来用密码模式,而这两种模式需要存的信息是不一样的,所以这个令牌要重新store一次。之后直接返回这个不过期的令牌。
如果令牌已经过期了或者说这个是第一次请求,令牌压根没生成,就会走下面的逻辑。
首先看看刷新的令牌有没有,如果刷新的令牌没有的话,那么创建一枚刷新的令牌。然后在121行根据authentication, refreshToken创建accessToken。而这个创建accessToken的方法也非常简单:
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
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;
}
OAuth2AccessToken其实就是用UUID创建一个accessToken,然后把过期时间,刷新令牌和scope这些OAuth协议规定的必须要存在的参数设置上,设置完了以后它会判断是否存在tokenEnhancer,如果存在tokenEnhancer它就会按照定制的tokenEnhancer增强生成出来的token。
拿到返回的令牌之后,在122行tokenStore会把拿到的令牌存起来,然后拿refreshToken存起来,最后把生成的令牌返回回去。
于是我们就获取到了令牌。
总结
以上源码参考个人项目 基于Spring Cloud、OAuth2.0开发基于Vue前后分离的开发平台