Token刷新并发处理解决⽅案
对 Token 进⾏刷新续期,我们要解决并发请求导致重复刷新 Token 的问题,这也是设计刷新 Token 的难点。这⾥我会分别介绍前端和后端各⾃的处理⽅案。
后端⽅案:利⽤ Redis 缓存
当同时发起多个请求时,第⼀个接⼝刷新了 Token,后⾯的请求仍然能通过请求,且不造成 Token 重复刷新。那么,后端在⽤户第⼀次登录时,需要将⽣成的 Token 数据(token 和 createTime)缓存⼀份到 Redis 中。
当 Token 过期时,重新⽣成新的 Token 数据并更新 Redis 缓存,同时在 Redis 中设置⼀条 Token 过渡数据并设置⼀个很短的过期时间(⽐如 30s)。如果后⾯的请求发现 Token 已经被刷新了,就判断 Redis 中是否存在 Token 过渡数据,存在就放⾏,这样同⼀时间的请求都可以通过。
pendingToken 刷新流程图
前端⽅案:请求拦截
由于前端请求都是异步的,只有⼀个请求的时候,刷新 Token 是⽐较好处理的,但并发请求下刷新 Token 处理起来有点⿇烦。我们需要考虑在多个请求⼏乎同时发起并且 Token 都失效的情况,当第⼀个请求进⼊ Token 刷新流程时,其他请求必须等待第⼀个请求完成 Token 刷新后再使⽤新 Token 进⾏重试。
简单地讲,就是同⼀时间有多个请求且 Token 都失效,在第⼀个请求进⾏ Token 刷新时,其他请求必须处于等待状态,直到 Token 刷新完成,才能携带新 Token 进⾏重试。
下⾯,我使⽤了 Angular 的请求,利⽤ BehaviorSubject 进⾏ Token 刷新状态的监听,当 Token 刷新成功,放⾏后⾯的请求进⾏重试。
除此之外,前端还可以利⽤ Promise,将请求存进队列中后,同时返回⼀个 Promise,让这个 Promise ⼀直处于 Pending 状态(即不调⽤ resolve),此时这个请求就会⼀直等待,只要我们不执⾏ resolve,这个请求就会⼀直在等待。当刷新 Token 的请求完成后 ,我们再调⽤ resolve,逐个重试。
Angular 代码⽰列
import{ Injectable }from"@angular/core";
import{
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
HttpErrorResponse
}from"@angular/common/http";
import{ throwError, Observable, BehaviorSubject,of}from"rxjs";
import{ catchError, filter, finalize, take, switchMap, mergeMap }from"rxjs/operators";
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private refreshTokenInProgress =false;
private refreshTokenSubject: BehaviorSubject<boolean>=new BehaviorSubject<boolean>(false);
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
if(!req.headers.has("Content-Type")){
req = req.clone({
headers: req.headers.set("Content-Type","application/json")
});
}
// 统⼀加上服务端前缀
let url = req.url;
if(!url.startsWith('')&&!url.startsWith('')){
url ="./"+ url;
}
req = req.clone({ url });
req =this.setAuthenticationToken(req);
return next.handle(req).pipe(
mergeMap((event:any)=>{
// 若⼀切都正常,则后续操作
return of(event);
}),
catchError((error: HttpErrorResponse)=>{
// 当是 401 错误时,表⽰ Token 已经过期,需要进⾏ Token 刷新
if(error && error.status ===401){
freshTokenInProgress){
// 如果 refreshTokenInProgress 为 true,我们将等到 refreshTokenSubject 是 true 时,才可以再次重试该请求
// 这表⽰刷新 Token 动作已完成,新 Token 已准备就绪
freshTokenSubject.pipe(
filter(result => result),
take(1),
switchMap(()=> next.handle(this.setAuthenticationToken(req)))
);
}else{
// 将 refreshTokenSubject 设置为 false,以便后⾯的请求调⽤时将处于等待状态,直到检索到新 Token 为⽌
<(false);
freshToken().pipe(
switchMap((newToken:string)=>{
<(true);
// 重新设置新的 Token
localStorage.setItem("token", newToken);
return next.handle(this.setAuthenticationToken(req));
}),
// 当刷新 Token 请求完成后,需要将 refreshTokenInProgress 设置为 false,⽤于下次刷新 Token
// 当刷新 Token 请求完成后,需要将 refreshTokenInProgress 设置为 false,⽤于下次刷新 Token
finalize(()=>(freshTokenInProgress =false))
);
}
}else{
return throwError(error);
}
})
);
}
private refreshToken(): Observable<any>{
// 这⾥需要换成实际的 Token 刷新接⼝
return of("JzdWIiOiJzdGFyIiwicm9sZSI6WyJST0xFX1VTRVIiXSwiaXNzIjoic2VjdXJpdHkiLCJpYXQiOjE2MDY 4MjczMDAsImF1ZCI6InNlY3VyaXR5LWFsbCIsImV4cCI6MTYwNjgzNDUwMH0.Hiq2DsH6j4XFd_v87lDWGlYembTLck7DjMLRLWdyvOo");
}
private setAuthenticationToken(request: HttpRequest<any>): HttpRequest<any>{
return request.clone({
headers: request.headers.set("Authorization","Bearer "+ Item("token"))
});
}
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论