流程分析
官方demo主要由两部分组成,tonr2(客户端)和sparklr2(服务端)。
学习曲线,spring
+spring mvc
+spring security
+spring security oauth
。
1)在客户端tonr2
- 客户端主界面上点击
View my Sparklr photos
调用org.springframework.security.oauth.examples.tonr.mvc.SparklrController
的/sparklr/photos
端点。它将通过一个sparklrService
调用获取照片。sparklrService
里面有装配sparklrRestTemplate
,向sparklr2
发送http://localhost:8080/sparklr2/photos?format=xml
请求。 - 调用
OAuth2RestTemplate.doExecute()
的第一步,就是从上下文(org.springframework.security.oauth2.client.DefaultOAuth2ClientContext
)中获取accessToken()
OAuth2AccessToken accessToken = context.getAccessToken();
- 第一次进来肯定是没有
accessToken
的,接下去,先没有判断,直接执行父类的doexcute。OAuth2RestTemplate
重载了createRequest()
方法,调用OAuth2RestTemplate.getAccessToken()
,去获取token
, - 从context中取出token,如果token不存在或者已经超时,则调用
OAuth2RestTemplate.acquireAccessToken()
方法。 - 接着调用
accessTokenProvider.obtainAccessToken(resource, accessTokenRequest)
,这个accessTokenProvider其实是一个提供者列表:
private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays.<AccessTokenProvider> asList(
new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));
obtainAccessToken()
方法,里面做了很多判断,比如如果存在clientTokenServices
的话,会先从存储中读取token
,如果过期,还有调用refresh_token
去刷新token
。都没有则调用了中会根据资源类型判断使用具体哪种provider,本例子调用org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider
的obtainAccessToken()
,然后因为没有授权码和状态码。抛出异常。
if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
// 抛出异常,交由OAuth2ClientContextFilter捕获,进行处理。
throw getRedirectForAuthorization(resource, request);
}
obtainAuthorizationCode(resource, request);
}
- 这里抛出
org.springframework.security.oauth2.client.resource.UserRedirectRequiredException
异常,异常将被org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter
的Catch处理。 - 然后进行会进行跳转
redirectUser(redirect, request, response)
;然后再到this.redirectStrategy.sendRedirect(request, response, builder.build().encode().toUriString())
;这里再向sparklr2
发http://localhost:8080/sparklr2/oauth/authorize?client_id=tonr&redirect_uri=http://localhost:8080/tonr2/sparklr/photos&response_type=code&scope=read%20write&state=BHPKRb
请求(授权请求) - 从这里开始获取获取授权码。
2)在服务端sparklr2(获取授权码流程)
- 经过
spring security的org.springframework.security.web.FilterChainProxy
过滤,用户没登录,将用户导向登录页面登录,(Spring Security的功能,不详细说明)登录完成后继续跳转到之前的获取授权码请求。org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize
接受请求,会判断是否已经授权:
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// 如果用户已经授权给了客户端
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
// 直接获取授权响应码
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// 否则还要导向用户到授权界面。
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
- 假设用户还没授权过给客户端,用户在界面选择是否授权并提交,
org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny
再接收请求,然后再响应获取授权码(当然用户都拒绝授权所有权限就会抛UserDeniedAuthorizationException
异常),同意则正常生成授权码,并保持授权状态信息,授权码会根据client_id
做对应保存,一个授权码只能使用一次,oauth2没有对授权码做超时处理,需要的话自己增加。再根据回调url回到客户端。
3)获取授权码的响应回到客户端 tonr2(重定向回到redirect_url)
- 回到
org.springframework.security.oauth.examples.tonr.impl.SparklrServiceImpl.getSparklrPhotoIds
再次发请求,此时又调用了sparklrRestTemplate
。跟第一步时一样的流程,区别在于判断授权码和授权状态码时,因为已经在请求中带有这两个参数,所以不会抛出异常,而是执行retrieveToken()
; - 调用
getRestTemplate().execute()
;这时就会向sparklr2发起获取accessToken
的请求http://localhost:8080/sparklr2/oauth/token
,这里发的是POST
请求,参数都在form
里面的。
4)在服务端获取 accessToken (获取token流程)
org.springfram work.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken
接收请求,处理生成accessToken
(是一个UUID,实际上包含的授权信息还是在服务端,只是这个UUID会对应Authentication
,如果是JWT的话,用户和角色以及权限范围都会放在JWT中,传输过来进行解码生成Authentication
,服务端不保留任何数据)。- 这个例子生成的accessToken在
org.springframework.security.oauth2.provider.token.DefaultTokenServices.createAccessToken()
,
然后调用tokenStore.storeAccessToken(accessToken, authentication)
,保存到服务端存储中,这里的tokenStore
使用InMemoryTokenStore
实现。(还有其他几种存储方式,例如JDBC, Redis以及JWT,可配置)。
5)获取accessToken的响应回到客户端
org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken
这个方法会将得到的accessToken保存到OAuth2ClientContext
。以后用户用这个accessToken来访问受保护的资源(直接访问资源服务端,当然这里授权服务端和资源服务端连在一起)就可以了.
6)访问受资源服务端保护的资源(前面没有特别说明的服务端都是指授权服务端)
- 走正常流程,用户获取完accessToken再访问受保护资源的跟踪,发起请求GET
http://localhost:8080/sparklr2/photos?format=xml
(OAuth2RestTemplate的context有保存accessToken,发请求时将这个accessToken放进了请求头) - 使用注解
@EnableResourceServer
配置资源服务器之后,会自动创建OAuth2AuthenticationProcessingFilter
,它用于将用户传过来的token验证,从存储找回用户的Authentication
,下面是它的doFilter方法主要内容:
//从请求获取accessToken,即那个UUID.并实例化为PreAuthenticatedAuthenticationToken对象
Authentication authentication = tokenExtractor.extract(request);
//authenticationManager为OAuth2AuthenticationManager的实例,
//它会调用OAuth2Authentication auth = tokenServices.loadAuthentication(token);
Authentication authResult = authenticationManager.authenticate(authentication);
//将Authentication存到spring security的上下文.以供后续使用
SecurityContextHolder.getContext().setAuthentication(authResult);
- 如果没有抛出异常则代表验证通过,可以访问受保护的资源了,当然还可以根据角色
ROLE
和范围SCOPE
进行限制,这个就跟Spring Security联系比较密切了,这里就暂时不分析了。
总结
总的来说,框架已经帮我们把OAuth2.0协议中的各个方面考虑清楚了,我们能自定义的内容主要集中在配置上,所以研究清楚每个配置的意义就比较重要了。