To renew the Token, we need to solve the problem of repeatedly refreshing the Token caused by concurrent requests, which is also the difficulty in designing the refreshing Token. Here I will introduce the front-end and back-end solutions respectively.
Back-end solution: Use Redis cache
When multiple requests are made at the same time, the first interface refreshes the Token, and subsequent requests can still pass the request without causing Token refreshes. When the user logs in for the first time, the backend caches a copy of the generated Token data (Token and createTime) into Redis.
When the Token expires, regenerate the Token data and update the Redis cache, while setting a Token transition data in Redis and setting a very short expiration time (such as 30s). If subsequent requests find that the Token has been refreshed, it checks whether there is Token transition data in Redis and permits it so that all requests at the same time can pass.
Source code address: github.com/yifanzheng/…
Token Refresh Flowchart
Front-end solution: request interception
Since front-end requests are all asynchronous, refreshing the Token is easier to handle when there is only one request, but it is a bit troublesome to process the refreshing Token in concurrent requests. We need to consider the case where multiple requests are initiated at almost the same time and all tokens are invalid. When the first request enters the Token refresh process, the other requests must wait for the first request to complete the Token refresh and then retry with the new Token. To put it simply, there are multiple requests at the same time and the Token is invalid. When the Token is refreshed for the first request, the other requests must wait until the Token refresh is complete. Then the request can be retried with the new Token.
Next, I use Angular’s request interceptor, and a BehaviorSubject called BehaviorSubject to monitor the Token refresh status. When the Token refresh is successful, I release all subsequent requests and retry.
In addition, the front end can take advantage of promises by placing the request in a queue and returning a Promise that will remain Pending until resolve is called. The request will be waiting. When the request to refresh the Token is complete, we call resolve and retry each one.
Github address: github.com/yifanzheng/…
The Angular code is listed
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(a)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")}); }// Add the server prefix
let url = req.url;
if(! url.startsWith('https://') && !url.startsWith('http://')) {
url = ". /" + url;
}
req = req.clone({ url });
req = this.setAuthenticationToken(req);
return next.handle(req).pipe(
mergeMap((event: any) = > {
// If all goes well, proceed
return of(event);
}),
catchError((error: HttpErrorResponse) = > {
// If the error is 401, the Token has expired and needs to be refreshed
if (error && error.status === 401) {
if (this.refreshTokenInProgress) {
// If refreshTokenInProgress is true, we wait until refreshTokenSubject is true before we can retry the request again
// This indicates that the Token refresh is complete and the new Token is ready
return this.refreshTokenSubject.pipe(
filter(result= > result),
take(1),
switchMap(() = > next.handle(this.setAuthenticationToken(req)))
);
} else {
this.refreshTokenInProgress = true;
// Set refreshTokenSubject to false so that future requests will be called in a waiting state until the new Token is retrieved
this.refreshTokenSubject.next(false);
return this.refreshToken().pipe(
switchMap((newToken: string) = > {
this.refreshTokenSubject.next(true);
// Reset the Token
localStorage.setItem("token", newToken);
return next.handle(this.setAuthenticationToken(req));
}),
// When the Token refresh request is complete, refreshTokenInProgress needs to be set to false for the next Token refresh
finalize(() = > (this.refreshTokenInProgress = false))); }}else {
returnthrowError(error); }})); }private refreshToken(): Observable<any> {
// The actual Token refresh interface needs to be replaced
return of("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdGFyIiwicm9sZSI6WyJST0xFX1VTRVIiXSwiaXNzIjoic2VjdXJpdHkiLCJpYXQiOjE2MD Y4MjczMDAsImF1ZCI6InNlY3VyaXR5LWFsbCIsImV4cCI6MTYwNjgzNDUwMH0.Hiq2DsH6j4XFd_v87lDWGlYembTLck7DjMLRLWdyvOo");
}
private setAuthenticationToken(request: HttpRequest<any>): HttpRequest<any> {
return request.clone({
headers: request.headers.set("Authorization"."Bearer " + localStorage.getItem("token"))}); }}Copy the code