【长文剖析】Spring Cloud OAuth 生成Token 源码解析

内容较长,spring security oauth 整个放发过程的类都有详细说明,建议你们保存后 慢慢阅读,或者当工具书查询html

Spring Security OAuth核心类图解析

关于Oauth2是什么以及Oauth2的四种受权模式请移步Oauth2官网java

下面简单介绍一下关于Spring Security OAuth基本的原理。这也是理解pig及其pigx的第一步。git

下面这张图涉及到了Spring OAuth的一些核心类和接口。spring

20189104922

上图蓝色的方块表明执行过程当中调用的具体的类,绿色的方块表明整个执行流程中调用的类,绿色的括号中表明的是该接口调用的具体的实现类。数据库

整个流程的入口点是在TokenEndpoint,由它来处理获取令牌的请求,获取令牌的请求默认是**/oauth/token**这个路径。bash

  • 1.当TokenEndpoint收到请求时,它首先会调用ClientDetailsService,ClientDetaisService从名字上看就很能够知道是一个相似于UserDetailsService的接口,只不过UserDetailsService读取的是用户的信息,而ClientDetailsService读取的是第三方应用的信息。app

  • 2.登陆请求头中带上Client的信息,而这个类就能够作到根据ClientId读取相应的配置信息。而ClientDetailsSevice读取到的信息都会封装到ClientDetails这个对象中。dom

  • 3.同时,TokenEndpoint还会建立一个TokenRequests的对象,这个对象中封装了除了第三方应用之外的其余信息。好比说grant_type,scope,username,password(限密码模式)等等信息,而这些信息都是封装在TokenRequests里面的。同时,ClientDetails也会被放到TokenRequests中,由于第三方应用的信息也是令牌请求的一部分。curl

  • 4.以后利用TokenRequests去调用一个叫作TokenGranter的令牌受权者的接口,这个接口实际上是对四种不一样的受权模式进行的一个封装。在这个接口里,它会根据请求传递过来的grant_type去挑一个具体的实现来执行令牌生成的逻辑。ide

  • 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
复制代码

回车之后咱们能够看到首先会通过网关的密码解密过滤器,而且参数通过咱们的一通改造以后已经能够获取到正确的值了。

20189192927

通过上面的一通操做,咱们已经拿到了获取token的一些必要的请求了。clientId,clientSecret,grant_type,usename,password,scope,终于能够带着咱们的参数深刻源码啦!

这里结合上文提到的核心类图来看效果更好

上文提过,OAuth2.0的认证的入口点位于TokenEndPoint。咱们也能够看到,代码确实已经进来了。

201891105014

咱们能够看到这个类上有一个@RequestMapping注解,它来处理/oauth/token的POST请求。

  1. 进来以后的第一步,就是在代码的95行,获取请求头中的clientId。
  2. 而后在96行调用getClientDetailsService().loadClientByClientId(clientId)方法获取整个第三方应用的详细配置。

201891105839

具体的参数的意义能够看spring-oauth-server 数据库表说明

  1. 在拿到客户端的信息以后在代码的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了,后面的代码很简单了:

201891111934

无非就是对下面这些参数的校验:

  • clientId:是否有值,值是否和查询结果匹配
  • scope:请求的一些受权内容,所请求的受权必须是第三方应用能够发送的受权集合的子集,不然没法经过校验)
  • grant_type:必须显式指定按照哪一种受权模式获取令牌
  • 判断传递的受权模式是不是简化模式,若是是简化模式也会抛异常。由于简化模式实际上是对受权码模式的一种简化:在用户的第一步的受权行为的时候就直接返回令牌,因此是不会有调用请求令牌服务的机会的
  • 判断是否是受权码模式,由于受权码模式包含两个步骤,在受权码模式中发出的令牌中拥有的权限不是由发令牌的请求决定的,而是在发令牌以前的受权的请求里就已经决定好了。所以它会对请求过来的scope进行置空操做,而后根据以前发出去的受权码里的权限从新设置你的scope,所以它根本不会使用请求令牌的这个请求中携带的scope参数。
  • 以后判断是否是刷新令牌的请求,应为刷新令牌的请求有本身的scope,因此也会进行从新设置scope的操做。

通过一系列的校验以后,最终TokenRequest会在132行传递给TokenGranter,而后由granter产生最终的accessToken。以后直接将accessToken写入响应里就能够了。

TokenGranter中总共封装了四种受权模式加一个刷新令牌的操做,咱们看看其中的一些细节。

201891114811

CompositeTokenGranter中有一个集合,这个集合里封装着的就是五个会产生令牌的操做。

它会对遍历这五种状况,并根据以前请求中携带的grant_type在五种状况中挑一种进行最终的accessToken的生成。

而后咱们看这个代码的第38行的具体的grant方法。

201891115533

20189112212

首先在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

这个方法不一样的受权模式会有不一样的实现。

201891121611

Spring Security OAuth核心类图解析中咱们已经知道最终产生的Oauth2Authorization包含两部分信息,一部分是请求中的一些信息,另外一部分是根据请求获取的受权用户的信息。而在不一样的受权模式下获取受权用户的信息的方式是不一样的,好比说pigx所使用的密码模式就是使用请求中携带的用户名和密码来获取当前受权用户中的受权信息,而在受权码模式的两个步骤中是根据第一步发出受权码的同时会记录相关用户的信息,以后对第二步进行受权的时候根据第三方应用请求过来的受权码再读取该受权码对应的用户信息。因此getOAuth2Authentication对于不一样的受权类型有不一样的实现。

咱们以pigx所使用的密码模式继续下面的流程。密码模式对应的是org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter

201891123119

而这个方法咱们能够看到它其实就是根据所请求的用户名和密码去建立UsernamePasswordAuthenticationToken,而后传递给authenticationManager作认证,在这个认证过程当中它会去调用com.pig4cloud.pigx.common.security.service.PigxUserDetailsServiceImplloadUserByUsername方法,根据用户名和密码去读取用户的信息,以后咱们其实就已经拿到Authorization的信息,而Oauth2Request根据第85行咱们能够知道是根据传进来的第三方应用详情和tokenRequest产生出来的,而86行的OAuth2Authentication也是由Oauth2RequestAuthorization这两个对象拼接起来的。而拼接的方式就是调用 org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactorycreateOAuth2Request方法。

public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) {
    return tokenRequest.createOAuth2Request(client);
}
复制代码

这个方法最终会建立一个由clientDetails和tokenRequest组合而成的OAuth2Request。

201891125353

拿到OAuth2Request就能够去生成OAuth2Authentication了。

OAuth2Authentication就是org.springframework.security.oauth2.provider.token.AbstractTokenGranter第71到73行最终传递进去生成accessToken的对象。

而OAuth2Authentications生成成功以后进行返回的话就能够执行AuthorizationServerTokenServicescreateAccessToken方法,而一旦这个access token生成成功并写入响应进行返回那么整个流程也就结束了,最终咱们就拿到了想要的访问令牌。

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

具体建立accessToken的代码,咱们须要仔细读一读org.springframework.security.oauth2.provider.token.DefaultTokenServicescreateAccessToken方法。

2018911396

首先这个类一进来就会尝试在tokenStore中获取accessToken,由于同一个用户只要令牌没过时那么再次请求令牌的时候会把以前发送的令牌再次发还。所以一开始就会找当前用户已经存在的令牌。

若是已经发送的令牌不为空,那么会在87行判断当前的令牌是否已通过期,若是令牌过时了,那么就会在tokenStore里把accessToken和refreshToken一块儿删掉,若是令牌没过时,那么就把这个没过时的令牌从新再存一下。由于可能用户是使用另外的方式来访问令牌的,好比说一开始用受权码模式,后来用密码模式,而这两种模式须要存的信息是不同的,因此这个令牌要从新store一次。以后直接返回这个不过时的令牌。

若是令牌已通过期了或者说这个是第一次请求,令牌压根没生成,就会走下面的逻辑。

201891132620

首先看看刷新的令牌有没有,若是刷新的令牌没有的话,那么建立一枚刷新的令牌。而后在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存起来,最后把生成的令牌返回回去。

因而咱们就获取到了令牌。

201891133652

总结

欢迎关注咱们得到更多的好玩JavaEE 实践

相关文章
相关标签/搜索