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

问题描述 投票:0回答:3

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

我没有找到太多关于如何使用 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;
  })
);
angular jwt angular-http-interceptors
3个回答
24
投票

你必须区分所有的请求。例如,您不想拦截您的登录请求,也不想拦截刷新令牌请求。 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));
                    })
                );
        }
    }

我们在拦截器类上有一个属性,用于检查是否已经有刷新令牌请求正在运行:

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
部分。希望现在更清楚了


2
投票

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

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

1) 如果单个请求由于 401 失败,则会调用刷新 令牌并将使用更新的令牌调用失败的 API。

2)如果多个请求由于401而失败,则会调用刷新 令牌并将使用更新的令牌调用失败的 API。

3)不会重复调用token API

如果仍然有人发现此代码不起作用的新场景,请通知我,以便我进行相应的测试和更新。

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));
                }));
        }
    }
}

0
投票

你可以做这样的事情:

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({});
    }
  }
));
© www.soinside.com 2019 - 2024. All rights reserved.