ZPY博客

Spring Security OAuth2.0 官方例子详解

这几天需要用到单点登录,准备学习下Spring Security OAuth2.0

官方例子:https://github.com/spring-projects/spring-security-oauth

按照步骤项目跑进来后没什么问题,但是动作的过程不是太清楚,于是开始读代码。但是发现看了几遍还是一脸懵。。google后发现很少有文章写这个例子的解析,有也是泛泛而谈。

对着一些网上的解析和自己debug,终于对整个过程有了一个大致的认识。

项目跑起来后,访问http://localhost:8080/tonr2即可进入tonr的登录界面,点击login后就登录进了tonr系统,这时我们点View my Sparklr photos链接时,会到sparklr的登录界面,登录后会让我们授权,然后会返回sparklr的相片详细画面。

结合代码,具体的过程是这样的。

当我们点击View my Sparklr photos链接时,链接的地址是"/sparklr/photos"。

当tonr系统接收后这个请求时,会进入到SparklrController.java的photos方法里,因为该方法匹配了@RequestMapping("/sparklr/photos"),在此方法里会调用sparklrService.getSparklrPhotoIds()函数。

getSparklrPhotoIds()方法的实现在SparklrServiceImpl.java里。tonr里最核心的代码就是此类的getSparklrPhotoIds方法。

sparklrRestTemplate.getForObject(
URI.create(sparklrPhotoListURL), byte[].class)

这里的sparklrPhotoListURL实际上就是这个地址http://localhost:8080/sparklr2/photos?format=xml

这个地址实际上是请求sparklr系统里获取相片详细。

这个地址在哪定义的?tonr工程里的sparklr.properties文件里定义了几个需要用到的地址,除了上面这个外还包括/oauth/authorize和/oauth/token等等。

上面这行代码会调用RestTemplate类的getForObject方法。(这里需要注意:sparklrRestTemplate的类型为RestOperationsRestOperations是一个接口,而RestTemplateRestOperations的实现类)

getForObject方法的最后会调用execute方法,execute方法实际上调用的是doExecute方法。

(这里我没有搞懂:应该执行RestTemplate里doExecute方法才对啊但是根据实际debug的结果,居然是进了OAuth2RestTemplate.java类的doExecute方法里OAuth2RestTemplate类是RestTemplate的子类。估计是用了反射吧。)

好吧,下面来看OAuth2RestTemplatedoExecute方法里都做了些什么。

首先第一行代码

OAuth2AccessToken accessToken = context.getAccessToken();

会去DefaultOAuth2ClientContext取得一个accessToken,那么这个accessToken是什么呢?

简单来说,accessToken就是令牌,当用户登录了sparklr系统并授权后就会生成一个accessToken,再之后tonr凭这个accessToken就可以拿到sparklr里的相片。

好了,第一次accessToken肯定是为null的,接下来代码会直接执行父类RestTemplate的doexcute方法。

RestTemplate的doexcute方法里会执行createRequest方法,如上面一样,会执行OAuth2RestTemplate类里的createRequest方法。(不太懂)

OAuth2AccessToken accessToken = getAccessToken();

看到上面的代码了吧?getAccessToken就是取得token的核心代码。

但是spring在这个方法里又嵌套了很多层,并没有所有代码都写在这个方法里。相信看过Spring源码的同学一定对这种方式不陌生。

getAccessToken里又调用了acquireAccessToken方法。而acquireAccessToken里又调用了obtainAccessToken方法。

这里要注意的是,obtainAccessToken方法实现执行的是

AuthorizationCodeAccessTokenProvider类里的方法。

这个方法里有一个判断,如下:

if (request.getAuthorizationCode() == null) {
      if (request.getStateKey() == null) {
        throw getRedirectForAuthorization(resource, request);
      }
      obtainAuthorizationCode(resource, request);
    }

第一次进来这两个if肯定都满足,执行getRedirectForAuthorization方法,到方法里去看,会发现实际抛出的是UserRedirectRequiredException异常。

这个异常会被OAuth2ClientContextFilter类的doFilter方法的Catch捕获。接下来会执行redirectUser方法。redirectUser里又会执行

this.redirectStrategy.sendRedirect(request, response, builder.build()
.encode().toUriString());

这行代码重定向到另一个Url,其实就是带了几个参数。在debug状态下我把url的值监视了下。结果如下:

http://localhost:8080/sparklr2/oauth/authorize?client_id=tonr&redirect_uri=http://localhost:8080/tonr2/sparklr/photos&response_type=code&scope=read%20write&state=60d5ek

可以看到,由于我们没有登录,所以这时重定向到了单点登录的验证地址并加上了几个相应的参数(client_id,redirect_uri,respondse_type等)。

接下来就是验证的过程了。这时会进到AuthorizationEndpoint类的authorize方法里,因为此方法匹配了@RequestMapping(value = "/oauth/authorize")

这个方法里的核心代码如下:

// Validation is all done, so we can check for auto approval...
      if (authorizationRequest.isApproved()) {
        if (responseTypes.contains("token")) {
          return getImplicitGrantResponse(authorizationRequest);
        }
        if (responseTypes.contains("code")) {
          return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
              (Authentication) principal));
        }
      }

      // Store authorizationRequest AND an immutable Map of authorizationRequest in session
      // which will be used to validate against in approveOrDeny()
      model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
      model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));

可以看到我们上面的重定向地址里的respondse_type参数指定为了code,所以上面代码的if (responseTypes.contains("code")) 将会满足,这时会执行getAuthorizationCodeResponse方法,getAuthorizationCodeResponse里会调用generateCode方法,而generateCode会调用RandomValueAuthorizationCodeServices类的createAuthorizationCode方法来生成授权码。

最后会返回redirect_uri。

所以会再次进入到getSparklrPhotoIds方法里,把最开始的那些代码再走一遍。这时拿到了授权码,但是accessToken还是空的。走到AuthorizationCodeAccessTokenProvider类的obtainAccessToken方法时,中间的过程就跳过了,与最上面的过程一样。

if (request.getAuthorizationCode() == null) {
      if (request.getStateKey() == null) {
        throw getRedirectForAuthorization(resource, request);
      }
      obtainAuthorizationCode(resource, request);
    }
    return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
        getHeadersForTokenRequest(request));

还是上面的代码,这时AuthorizationCode不为null,所以if不满足,直接执行最后一句代码,这行代码会请求获得accessToken,请求地址为http://localhost:8080/sparklr2/oauth/token

当然还有一些必要的参数,当然其中最重要的参数就是授权码code。

这时会进入到TokenEndpoint类的postAccessToken方法里,因为此方法去匹配了@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)

此方法最终生成token的是下面这一行代码:

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

这时会再次访问http://localhost:8080/sparklr2/photos?format=xml,因为已经有了accessToken,所以可以直接调用sparklr工程里的PhotoController类的getXmlPhotos方法了。

至此,基本的过程就走完了。之后的问题就是如何用这个框架来运用到自己项目上了。