写在前面
出于个人兴趣,最近OAuth2源码看的有点多,同时也是思考了几个由浅入深的面试点,
这里结合源码一并分析解答下,也能让我们更好的理解OAuth2整体架构.......
1. 授权码authorization-code生成过程,生成算法,授权码信息如何存储
1.1 client第三方应用,请求认证服务器获取授权码接口,如 localhost:7777/oauth/authorize?client_id=cms&response_type=code
之后会跳转到 认证服务器的首页登陆地址,进行认证服务器的登陆;
这里调用获取授权码,跳转到认证服务器登陆首页,详细源码解读,可以见我的另一片文章
OAuth2源码解析之 认证服务器(Authorization server) 授权, 登陆,跳转首页流程
1.2 在认证服务器登陆页输入账号密码(认证服务器预先颁发给client第三方应用的账号密匙) ,验证成功,进入到授权页
1.3 成功进入到授权页,选择权限进行授权
1.4 追踪核心源码类AuthorizationEndpoint
return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal
继续追踪 String code = authorizationCodeServices.createAuthorizationCode(combinedAuth);
继续追踪到 RandomValueAuthorizationCodeServices类,这一步即看到了授权码的生成,想要深入的同学可以继续研究一下这个生成算法,核心代码见下(作者算法功底有限,个人意会😄)
class RandomValueStringGenerator{
public String generate() {
byte[] verifierBytes = new byte[length];
random.nextBytes(verifierBytes);
return getAuthorizationCodeString(verifierBytes);
}
protected String getAuthorizationCodeString(byte[] verifierBytes) {
char[] chars = new char[verifierBytes.length];
for (int i = 0; i < verifierBytes.length; i++) {
chars[i] = DEFAULT_CODEC[((verifierBytes[i] & 0xFF) % DEFAULT_CODEC.length)];
}
return new String(chars);
}
}
1.5 生成授权码后,将此次授权码信息进行存储 store(code, authentication).
同时大家也看到了OAuth2 默认授权码存储方式,以及后续的access_token;
内部提供了两种 一种是基于内存的 InMemoryAuthorizationCodeServices(默认);
另一种则是需要拓展的JDBC JdbcAuthorizationCodeServices;
我们可以看到Oauth2将此次的授权码信息存储在 ConcurrentHashMap<String, OAuth2Authentication> authorizationCodeStore
1.6 Over获取到授权码
2. 授权码使用时,可以使用几次?(通过授权码获取access_token)
答案:一次/用完即弃
2.1 通过上述1.5 我们知道授权码的信息保存在内存InMemoryAuthorizationCodeServices ConcurrentHashMap<String, OAuth2Authentication> authorizationCodeStore中
我们通过postman来模拟获取通过授权码获取access_token请求(http://localhost:7777/oauth/token?grant_type=authorization_code&code=njekXY&client_id=cms&client_secret=secret)
观察源码 核心入口类:TokenEndpoint
2.2 核心入口 获取access_token OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest)
getAccessToken(client, tokenRequest)--->tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));---->OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
2.3 观察consumeAuthorizationCode(String code)这个方法做了什么,
发现 步骤1.5 保存的授权码信息被取出来了,然后remove 删除掉了
OAuth2Authentication auth = this.remove(code);
@Override
public OAuth2Authentication remove(String code) {
OAuth2Authentication auth = this.authorizationCodeStore.remove(code);
return auth;
}
也就是此次,通过授权码njekXY 匹配拿到了步骤1.5保存的信息,之后再将此内存信息删除,那么下一次同样的授权码njekXY再次进入此方法,获取的数据就是null,也就是无法获取到access_token,即 授权码只能使用一次
这是第一次通过授权码获取access_token的结果:
2.4 我们再次通过通过授权码njekXY 获取access_token看一下,(验证是否只能使用一次)
此次获取的信息为null 所以直接抛出了异常throw new InvalidGrantException("Invalid authorization code: " + code);
最终错误被 TokenEndpoint类 异常捕获,返回了获取access_token失败
3. 为什么要先获取授权码再获取access_token,为什么不直接获取access_toke
先看官方文档(RFC 6819)
Authorization "codes" are sentto the client’s redirect URI instead of tokens for two purposes:
- Browser-based flows expose protocol parameters to potential attackers via URI query parameters (HTTP referrer), the browser cache, or log file entries, and could be replayed. In order to reduce this threat, short-lived authorization "codes" are passed instead of tokens and exchanged for tokens over a more secure direct connection between the client and the authorization server.
- It is much simpler to authenticate clients during the direct request between the client and the authorization server than in the context of the indirect authorization request. The latter would require digital signatures.
个人理解:
1 我们看到上述过程,授权码是通过重定向方式 http://localhost:7000/?code=njekXY 返回给第三方应用的,如果我们直接返回的是access_token,那么相当于access_token被间接的暴露出来,暴露出来的access_token存在安全威胁,这样设计则是为了减少这种威胁
2 回忆获取access_token的步骤,我们是先登陆认证中心地址,然后进入授权页面,再点击相应权限确认赋予权限,获取授权码,再操作
如果client第三方想直接获取access_token,可以想像这个web请求 整体耗费时间是很长的,web系统无法长时间等待,否则应用也会一直阻塞,各种问题接踵而至
4. 为什么授权码只能使用一次
同问题3,其实是出于安全角度,试想如果一个授权码如果能重复性使用,
那么黑客拿到了授权码后给你写个攻击脚本,整个暴力碰撞,access_token被获取, 就会出现安全性问题.
因此每一个code只能使用一次,用完作废,也是为了提升安全性.