关于JWT遇到的一些前后端问题,用Redis进行解决
第一次使用JWT这种方案。 也遇到了不少问题。以下是我的解决方案,希望可以帮助到他人。
1.解决Token过期
我的token 分为两种,一种是AccessToken,一种是RefreshToken。 AccessToken被指定用来访问受保护的API,过期时间短,我的为30分钟。 RefreshToken被用于重新获取,过期时间较长,我的为7天。 重新获取Token是通过中间件实现的。 在中间件中,拿到RefreshToken进行校验, 如果通过则直接返回新的AccessToken与RefreshToken。 RefreshToken可以视情况返回,例如RefreshToken过期时间小于一定时间就生成新的RefreshToken,也可以每次都生成新的RefreshToken。
1.1RefreshToken过期怎么办?
在上面的逻辑中,客户端必须要持续调用刷新Token的API或中间件,RefreshToken在AccessToken被刷新的同时也会被刷新。这样就能实现持续不断的保持Token可用状态。如果RefreshToken过期,说明得到这个Token的用户已经至少于RefrshToken保质期这么长一段时间内没有进行过任何请求。 那么理应要求用户重新登录,获取授权。
2.如何强制某个Token无效?
我的做法是,通过Redis存储来进行标记,过期时间就为Token本身的过期时间。 例如在用户进行登出操作之后,将用户在登出操作提交的AccessToken与RefreshToken存入Redis中。然后在中间件中进行查询,如果AccessToken或者RefreshToken存在,则说明此token是无效的,应该返回401状态码。 关于Token失效的问题,可能是这个解决方案的最大缺点了。 不过仔细一想,token的作用不正是如此吗?就如欢乐谷的套票,只要在规定时间内,持有票据的人是可以任意游玩设施的。只是我们处于业务逻辑的需要,必须让token失去作用。在我个人看来,token其实是保护Web API的一个比较好的方案。借助类似Redis之类的工具,也是不错的。
2.1 登出之前,可能存在多次RefreshToken刷新的过程,如何保证这些RefreshToken也失效?
这个问题也困扰了我很久,目前有两种思路, 一是在RefreshToken尽可能接近过期时间之后再刷新RefreshToken。缩短RefreshToken作用时间,但是这样的话可能出现用户体验感不友好,即使是真正的活跃用户,也可能会出现需要重新登录的情况。
二是在RefreshToken被创建的时候,就以UserId和User-Agent一起作为Key,RefreshToken作为Value,以RefreshToken过期时间 存入Redis当中,每次用户在进行Token刷新的时候,也更新Redis中的value。如果用户Id和UA查询的结果和Refresh携带的RefreshToken结果不符合,则拒绝刷新。
3.用户在进行改密码之类的安全操作之后,如何使所有的Token失效
与第二点的场景不同的是,用户可能在多个设备进行过登录,也就是拿到过多个token。在某一个设备上进行注销,不应该影响其他设备。所以可以直接在注销时,将所使用的token存如黑名单。而用户进行安全操作之后,则应该将用户Id存入Redis,然后在中间件中进行处理,返回401。登录的接口也稍作修改,从Redis中移除Key。
4.前端如何进行用户无察觉的Token刷新
我前端使用的是Angular。有个东西叫拦截器,可以很方便的进行Http请求的可以说叫预处理吧。 在拦截器里,我们可以监测到是否有token,和token是否过期。JWT除了签名部分是加密的,其他的部分是可以被解码的。如果token过期了,我们就在这个请求发送之前,先去服务器刷新一下新的token。然后再用新的token去继续我们的这个请求。
//这个拦截器里就有这样一个方法
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
//next参数有一个handle()方法,返回一个Observable对象
//如果不返回,则会使拦截器短路。
这个拦截器咋一看,非常像咱后台的中间件管道。 但是这个方法不是异步的,我也不是特别了解Js的异步。所以不能在调用next.handle()方法之前,等待刷新Token的结果。 后来我看到一个人,提供了这样一个思路。 我们先new一个Subject给拦截器,让他返回Subject。 然后再放心的去请求新的Token。拿到之后,再使原来的请求重现。
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
//如果请求为刷新Token,不做处理
if (req.headers.has("Re_T")) {
return next.handle(req);
}
let token = localStorage.getItem(AccessToken);
//如果token没在localStorage中,咱就不管了
if (token == null) {
return next.handle(req);
}
//如果AccessToken未过期,直接添加Token到请求中
if (!this.IsTokenExpire(token)) {
let auth = req.clone({ setHeaders: { "Authorization": "Bearer " + token } })
return next.handle(auth);
} else {
let refreshToken = localStorage.getItem(RefreshToken);
//如果RefreshToken过期,咱也不管了
if (this.IsTokenExpire(refreshToken)) {
this.accountExpire.emit();
return next.handle(req);
} else {
//先生成一个subject对象。
const subject = new Subject<any>();
this.reTokenAsync(req, next, subject, refreshToken)
// 返回被委托的对象,让真正的业务请求隐匿起来。
return subject;
}
}
}
// 重新获取token
async reTokenAsync(req: HttpRequest<any>, next: HttpHandler, sub: Subject<any>, refreshToken: string) {
const res = await this.Refresh(refreshToken);
localStorage.setItem(AccessToken, res.content.accessToken);
localStorage.setItem(RefreshToken, res.content.refreshToken);
const request = req.clone({
setHeaders: {
'Authorization': "Bearer " + res.content.accessToken,
}
});
// 让原本请求重新出现
next.handle(request).pipe(
tap(data => {
sub.next(data); // 数据到达,转达下发
return data;
}, (error) => {
sub.error(error); //数据报错,转达出错
})
).subscribe(); //由于该Observable对象已经没有人去主动订阅它了。所以我们手动订阅一下
}
- 0
- 0
-
分享