设计需求
- 统计在线列表,可以随时的踢下线,让客户端缓存的token失效
- 30天内无需重新登录,默认可无限期延续
Token生成方案
JWT(JSON WEB TOKEN)的构成
第一部分我们称它为头部(header),
第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),
第三部分是签名(signature确保数据的完整性).JWT token = header.payload.signature
由于JWT是无状态的,也未能实现我们上述的需求,所以不得不放弃了JWT的方案,但是其有关token的加密算法还是值得参考的。
access_token生成方案
1 | payload = urlencode(base64({uid,ts})) |
refresh_token生成方案
1 | payload = urlencode(base64({uid,ts})) |
算法实现:
1 | import org.apache.commons.codec.binary.Base64; |
token生成算法
1 | 明文:{"uid":10010,"ts":1540912751948} |
token校验算法
签名校验
1
2
3
4token:eyJ1aWQiOjEwMDEwLCJ0cyI6MTU0MDkxMjc1MTk0OH0%3D.0ba40cff2bda8fab9ae61893b43c198e3022a1e8f058ac8054417241d3129d1f
payload密文:eyJ1aWQiOjEwMDEwLCJ0cyI6MTU0MDkxMjc1MTk0OH0%3D
signature签名:0ba40cff2bda8fab9ae61893b43c198e3022a1e8f058ac8054417241d3129d1f
签名校验:true时间戳校验
1
2
3payload解密:{"uid":10010,"ts":1540912751948}
690 ms
有效期内,开始进行redis查询
注意:
这个也是我经过思考后最终确认下来的token生成算法,也许你也发现既然redis持久化了就直接查库即可,无需搞的这么复杂的算法。实际上我是这样考虑的,因为大量的api请求都会带token参数导致每次请求都会查询库,固然redis的强大的性能足以支持,但还是为了效率过滤一些没有必要的io资源。
- 得到请求的时候首先验证signature是否匹配,防止篡改
- 再从payload中获取ts以判断时间戳是否失效,access_token 2hr, refresh_token 30days.
- 前两者校验完成,则请求一次redis io做最终的判断。当然排除临时被提下线的情况下,大部分情况下这里都是通过的。
- redis key 的设计,其中的uid设计主要是为了后期统计使用,如:
1
2
3
4
5
6
7
8
9
10
11
12keys (refresh_token:uid:*)获取当前终端连接数,
Set<String> setz = jedis.keys("access_token:*");
System.out.println("access_token:* key 的数目:"+setz.size());
获取当前在线用户数(根据uid排重)
Set<String> setzz = new HashSet<String>();
for(String str:setz) {
setzz.add(str.split(":")[1]);
}
System.out.println("key uid 去重:"+setzz.size()); - redis ttl 就不用不多说了,access_token 2hr, refresh_token 30days.
- redis value:whatever anythings
Redis 有状态缓存
access_token
key | ttl | value |
---|---|---|
access_token:10024:ak_1 | 2hr | username:zhangsan,uid:10024 |
access_token:10025:ak_2 | 2hr | username:lisi,uid:10025 |
refresh_token
key | ttl | value |
---|---|---|
refresh_token:10024:rk_1 | 30days | access_token:10024:ak_1 |
refresh_token:10025:rk_2 | 30days | access_token:10025:ak_2 |
拓扑结构
用户授权获取token
请求方法:
https://apihost/oauth2/access_token?username=zhangsan&password=123456
正确的返回:
1 | { |
参数 | 说明 |
---|---|
access_token | 接口调用凭证 |
refresh_token | 用户刷新access_token |
uid | 授权用户唯一标识 |
expires_in | access_token接口调用凭证超时时间,单位(秒) |
错误返回样例:
{"code":400,"msg":"bad request"}
刷新access_token有效期
access_token是调用授权关系接口的调用凭证,由于access_token有效期(目前为2个小时)较短,当access_token超时后,可以使用refresh_token进行刷新,用refresh_token仅能使用一次,使用一次后,将被废弃。也就是说refresh_token 更新 access_token 的时候 refresh_token 也会跟着被更新
- 通过用refresh_token机制可以确保活跃用户长期不用登录授权。
- refresh_token拥有较长的有效期(30天),当refresh_token失效的后,需要用户重新授权。也就说第一次登录与第二次登录时长间隔30天以上则需要用户重新授权登录。
请求方法:
https://apihost/oauth2/refresh_token?uid=UID&refresh_token=REFRESH_TOKEN
正确的返回:
1 | { |
参数 | 说明 |
---|---|
access_token | 接口调用凭证 |
refresh_token | 用户刷新access_token |
uid | 授权用户唯一标识 |
expires_in | access_token接口调用凭证超时时间,单位(秒) |
错误返回样例:
{"code":400,"msg":"invalid refresh_token"}
通过access_token调用接口
获取access_token后,进行接口调用,有以下前提:
access_token有效且未超时;如果access_token失效,则通过refresh_token重新同步。若refresh_token也失效了则需要用户重新授权登录。
Eg.通过UID获取用户基本信息
请求方法
https://apihost/user?access_token=ak_1&uid=10024
正确的返回
1 | { |
错误返回样例:
{"code":400,"msg":"invalid access_token"}
注意事项RISK CONTROL
- access_token 为用户授权客户端发起接口调用的凭证(相当于用户登录态),存储在客户端,可能出现恶意获取access_token 后导致的用户数据泄漏、用户微信相关接口功能被恶意发起等行为;
- refresh_token 为用户授权客户端应用的长效凭证,仅用于刷新access_token,但泄漏后相当于access_token 泄漏,风险同上。
- 单点/多点登录。介于我们当前的设计不考虑单点登录的问题,也就是说同一个账号在不同的客户端都可以同时登录,并且生成不同的token以供使用每个客户端程序维护自己的一套token即可。倘若要实现单点登录则在用户授权的时候做排重判断,如果已在其他重点登录则清空该重点的token,并且为当前链接生成新token对即可。具体情况shi
- 用户信息跟新,针对我们当前的授权方式一旦用户修改的密码,则要清空该用户所有终端的token,让其重新授权。这也是为什么要设计这套有状态的token机制主要原因