防止在Angular中多次触发令牌刷新请求

时间:2020-10-09 03:22:34

标签: angular authentication jwt jwt-auth fork-join

我正在使用JWT构建基本的身份验证系统。以下是基本的工作流程:

enter image description here

在向API发送请求之前,我有一个HttpInterceptor,它检查令牌是否已过期。如果尚未过期,拦截器将通过JWT将授权标头附加到请求。如果已过期,则需要在发送实际请求之前刷新令牌。当刷新请求到达后端时,它将针对数据库检查刷新令牌,并删除条目,使其只能使用一次,然后将返回新的JWT +刷新令牌。

我的某些页面在访问它们时会触发多个请求,这就是问题所在。一次将发送多个参照请求,因此只有第一个到达后端的请求会成功返回。所有其他请求将返回401错误,这会导致客户端问题。

因此,我正在寻找一种方法来暂停所有请求,直到第一个刷新请求返回。 HttpInterceptor调用一个返回JWT的函数,方法是检查到期日期,并在到期时触发刷新请求。

令牌拦截器:

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Get access token
    this._auth.getAccessToken().subscribe((accessToken: string) => {
        request = request.clone({
            setHeaders: {
                Authorization: `Bearer ${accessToken}`
            }
        });
        return next.handle(request);
    });
}

获取访问令牌功能:

public getAccessToken(): Observable<string> {
    if (!this._authUser) return throwError("User is not logged in.");   // Make sure user is logged in

    // Refresh token
    if (this.checkTokenExpired())
        return this.refreshToken().pipe(map(() => this._authUser.tokenManager.accessToken));

    return of(this._authUser.tokenManager.accessToken);
}

对于模拟请求,此架构成为问题,因为刷新请求将被多次发送。我需要一个互斥锁之类的东西来停止第一个请求之外的所有请求,并在刷新令牌后释放所有请求并返回新的JWT。

1 个答案:

答案 0 :(得分:0)

由于以下博文,我得以获得所需的实现:https://stackoverflow.com/a/54328099/5203853

这是我对refreshToken()函数的实现的样子:

// Variables
private _tokenSubject: BehaviorSubject<auth.User> = new BehaviorSubject<auth.User>(null);
private _isRefreshingToken: Boolean = false;

private refreshToken(): Observable<auth.User> {
    if (!this._authUser) return throwError("User is not logged in."); // Make sure user is logged in

    if (!this._isRefreshingToken) {
        this._isRefreshingToken = true;
        // Reset such that the following requests wait until the token
        // comes back from the refreshToken call.
        this._tokenSubject.next(null);

        let req = this._httpClient.post<AuthService.authUser>('/api/auth/refresh', { 
            accessToken: this._authUser.tokenManager.accessToken,
            refreshToken: this._authUser.tokenManager.refreshToken 
        })

        return req.pipe(
            tap(authUser => {
                this.saveUser(authUser);
                // Emit event & return promise
                this.onAuthStateChange.emit(authUser.user);
                // Retry previous request
                this._tokenSubject.next(authUser.user);
            }),
            map(authUser => authUser.user),
            catchError((err: HttpErrorResponse) => {
                this.signOut();
                this._router.navigate(['/auth/login']);
                return throwError(err.message);
            }), 
            finalize(() => {
                this._isRefreshingToken = false;
            })
        );
    }
    else {
        return this._tokenSubject
            .pipe(
                filter(authUser => authUser != null), 
                take(1)
            );
    }
}