内容较长,spring security oauth 整个放发过程的类都有详细说明,建议大家保存后 慢慢阅读,或者当工具书查询
关于Oauth2是什么以及OAuth2的四种授权模式请移步Oauth2官网。
下面简单介绍一下关于Spring Security OAuth基本的原理。这也是理解pig及其pigx的第一步。
下面这张图涉及到了Spring OAuth的一些核心类和接口。
上图蓝色的方块代表执行过程中调用的具体的类,绿色的方块代表整个执行流程中调用的类,绿色的括号中代表的是该接口调用的具体的实现类。
整个流程的入口点是在TokenEndpoint,由它来处理获取令牌的请求,获取令牌的请求默认是**/oauth/token**这个路径。
下面的是一个标准的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请求。
getClientDetailsService().loadClientByClientId(clientId)
方法获取整个第三方应用的详细配置。具体的参数的意义可以看spring-oauth-server 数据库表说明
创建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了,后面的代码很简单了:
无非就是对下面这些参数的校验:
经过一系列的校验之后,最终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存起来,最后把生成的令牌返回回去。
于是我们就获取到了令牌。