尝试在 angular 7 中使用拦截器在刷新令牌后重复 http 请求

Trying to repeat a http request after refresh token with a interceptor in angular 7

我正在尝试在收到错误 401 angular 7 时自动执行刷新令牌请求。

我没有找到太多关于如何使用 angular 7 的文档,而且我以前不了解 angular 或 rxjs 我变得有点疯狂

我认为它几乎完成了,但由于某种原因,第二个 next.handle(newReq) 没有发送请求(在 google chrome 网络调试器中只出现第一个请求)

我正在获取刷新响应并使 processLoginResponse(res) 正确

你可以在这里看到我的拦截器

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

let newReq = req.clone();

return next.handle(req).pipe(
  catchError(error => {
    if (error.status == 401) {
      this._authenticationService.refresh().subscribe(
        res => {
          this._authenticationService.processLoginResponse(res);
          newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
          return next.handle(newReq)
        },
        error => {
          this._authenticationService.logOut();
        });
    }
    throw error;
  })
);

你可以这样做:

import { HttpErrorResponse } from '@angular/common/http';

return next.handle(req).pipe(
  catchError((err: any) => {
    if (err instanceof HttpErrorResponse && err.status 401) {
     return this._authenticationService.refresh()
       .pipe(tap(
         (success) => {},
         (err) => {
           this._authenticationService.logOut();
           throw error;
         }
       ).mergeMap((res) => {
         this._authenticationService.processLoginResponse(res);
         newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
         return next.handle(newReq)
       });
    } else {
      return Observable.of({});
    }
  }
));

您必须区分所有请求。例如,您不想拦截您的登录请求,也不想拦截刷新令牌请求。 SwitchMap 是你最好的朋友,因为你需要取消一些调用来等待你的令牌被刷新。

所以您要做的是首先检查状态为 401(未授权)的错误响应:

return next.handle(this.addToken(req, this.userService.getAccessToken()))
            .pipe(catchError(err => {
                if (err instanceof HttpErrorResponse) {
                    // token is expired refresh and try again
                    if (err.status === 401) {
                        return this.handleUnauthorized(req, next);
                    }

                    // default error handler
                    return this.handleError(err);

                } else {
                    return observableThrowError(err);
                }
            }));

在您的 handleUnauthorized 函数中,您必须刷新令牌并同时跳过所有进一步的请求:

  handleUnauthorized (req: HttpRequest<any>, next: HttpHandler): Observable<any> {
        if (!this.isRefreshingToken) {
            this.isRefreshingToken = true;

            // Reset here so that the following requests wait until the token
            // comes back from the refreshToken call.
            this.tokenSubject.next(null);
            // get a new token via userService.refreshToken
            return this.userService.refreshToken()
                .pipe(switchMap((newToken: string) => {
                    // did we get a new token retry previous request
                    if (newToken) {
                        this.tokenSubject.next(newToken);
                        return next.handle(this.addToken(req, newToken));
                    }

                    // If we don't get a new token, we are in trouble so logout.
                    this.userService.doLogout();
                    return observableThrowError('');
                })
                    , catchError(error => {
                        // If there is an exception calling 'refreshToken', bad news so logout.
                        this.userService.doLogout();
                        return observableThrowError('');
                    })
                    , finalize(() => {
                        this.isRefreshingToken = false;
                    })
                );
        } else {
            return this.tokenSubject
                .pipe(
                    filter(token => token != null)
                    , take(1)
                    , switchMap(token => {
                        return next.handle(this.addToken(req, token));
                    })
                );
        }
    }

我们在拦截器上有一个属性 class 检查是否已经有刷新令牌请求 运行: this.isRefreshingToken = true; 因为你不希望有多个刷新请求您触发了多个未经授权的请求。

因此 if (!this.isRefreshingToken) 部分中的所有内容都是关于刷新您的令牌并再次尝试之前的请求。

else 中处理的所有内容都是针对所有请求的,与此同时,当您的 userService 刷新令牌时,将返回一个 tokenSubject 并且当令牌准备就绪时 this.tokenSubject.next(newToken); 每个跳过的请求将重试。

这篇文章是拦截器的起源灵感:https://www.intertech.com/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/

编辑:

TokenSubject 实际上是一个行为主题:tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);,这意味着任何新订阅者都将获得流中的当前值,这将是我们上次调用 this.tokenSubject.next(newToken) 时的旧令牌。

next(null) 每个新订阅者都不会触发 switchMap 部分,这就是为什么 filter(token => token != null) 是必要的。

在使用新令牌再次调用 this.tokenSubject.next(newToken) 之后,每个订阅者都会使用新令牌触发 switchMap 部分。希望现在更清楚

编辑 2020 年 9 月 21 日

修复link

下面是调用刷新令牌的代码,获取刷新令牌后调用失败API,

源代码中的注释将帮助您理解流程。 它经过测试并且适用于以下场景

1) If single request fails due to 401 then it will called for refresh token and will call failed API with updated token.

2) If multiple requests fails due to 401 then it will called for refresh token and will call failed API with updated token.

3) It will not call token API repeatedly

如果还有人发现此代码无法正常工作的新情况,请通知我,以便我进行相应的测试和更新。

import { Injectable } from "@angular/core";
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse, HttpHeaders, HttpClient, HttpResponse } from "@angular/common/http";

import { Observable } from "rxjs/Observable";
import { throwError, BehaviorSubject } from 'rxjs';
import { catchError, switchMap, tap, filter, take, finalize } from 'rxjs/operators';
import { TOKENAPIURL } from 'src/environments/environment';
import { SessionService } from '../services/session.service';
import { AuthService } from '../services/auth.service';

/**
 * @author Pravin P Patil
 * @version 1.0
 * @description Interceptor for handling requests which giving 401 unauthorized and will call for 
 * refresh token and if token received successfully it will call failed (401) api again for maintaining the application momentum
 */
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {

    private isRefreshing = false;
    private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);


    constructor(private http: HttpClient, private sessionService: SessionService, private authService: AuthService) { }

    /**
     * 
     * @param request HttpRequest
     * @param next HttpHandler
     * @description intercept method which calls every time before sending requst to server
     */
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // Taking an access token
        const accessToken = sessionStorage.getItem('ACCESS_TOKEN');
        // cloing a request and adding Authorization header with token
        request = this.addToken(request, accessToken);
        // sending request to server and checking for error with status 401 unauthorized
        return next.handle(request).pipe(
            catchError(error => {
                if (error instanceof HttpErrorResponse && error.status === 401) {
                    // calling refresh token api and if got success extracting token from response and calling failed api due to 401                    
                    return this.handle401Error(request, next);
                } // If api not throwing 401 but gives an error throwing error
                else {
                    return throwError(error);
                }
            }));
    }

    /**
     * 
     * @param request HttpRequest<any>
     * @param token token to in Authorization header
     */
    private addToken(request: HttpRequest<any>, token: string) {
        return request.clone({
            setHeaders: { 'Authorization': `Bearer ${token}` }
        });
    }

    /**
     * This method will called when any api fails due to 401 and calsl for refresh token
     */
    private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
        // If Refresh token api is not already in progress
        if (this.isRefreshing) {
            // If refreshTokenInProgress is true, we will wait until refreshTokenSubject has a non-null value
            // – which means the new token is ready and we can retry the request again
            return this.refreshTokenSubject
                .pipe(
                    filter(token => token != null),
                    take(1),
                    switchMap(jwt => {
                        return next.handle(this.addToken(request, jwt))
                    }));
        } else {
            // updating variable with api is in progress
            this.isRefreshing = true;
            // Set the refreshTokenSubject to null so that subsequent API calls will wait until the new token has been retrieved
            this.refreshTokenSubject.next(null);

            const refreshToken = sessionStorage.getItem('REFRESH_TOKEN');
            // Token String for Refresh token OWIN Authentication
            const tokenDataString = `refresh_token=${refreshToken}&grant_type=refresh_token`;
            const httpOptions = {
                headers: new HttpHeaders({
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-Skip-Interceptor': ''
                })
            };
            return this.http.post<any>(TOKENAPIURL, tokenDataString, httpOptions)
                .pipe(switchMap((tokens) => {
                    this.isRefreshing = false;
                    this.refreshTokenSubject.next(tokens.access_token);
                    // updating value of expires in variable                    
                    sessionStorage.setItem('ACCESS_TOKEN', tokens.access_token);
                    return next.handle(this.addToken(request, tokens.access_token));
                }));
        }
    }
}