这几天需要用到单点登录,准备学习下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的类型为RestOperations,RestOperations是一个接口,而RestTemplate是RestOperations的实现类)
getForObject方法的最后会调用execute方法,execute方法实际上调用的是doExecute方法。
(这里我没有搞懂:应该执行RestTemplate里的doExecute方法才对啊,但是根据实际debug的结果,居然是进了OAuth2RestTemplate.java类的doExecute方法里,OAuth2RestTemplate类是RestTemplate的子类。估计是用了反射吧。)
好吧,下面来看OAuth2RestTemplate的doExecute方法里都做了些什么。
首先第一行代码
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方法了。
至此,基本的过程就走完了。之后的问题就是如何用这个框架来运用到自己项目上了。