トークンの更新後のAngular4インターセプターの再試行リクエスト


84

こんにちは私は401 unauthorized、トークンを更新してリクエストを再試行することで、新しい角度インターセプターを実装し、エラーを処理する方法を理解しようとしています。これは私がフォローしているガイドです:https//ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors

失敗したリクエストを正常にキャッシュし、トークンを更新できますが、以前に失敗したリクエストを再送信する方法がわかりません。また、これを現在使用しているリゾルバーで機能させたいと思います。

token.interceptor.ts

return next.handle( request ).do(( event: HttpEvent<any> ) => {
        if ( event instanceof HttpResponse ) {
            // do stuff with response if you want
        }
    }, ( err: any ) => {
        if ( err instanceof HttpErrorResponse ) {
            if ( err.status === 401 ) {
                console.log( err );
                this.auth.collectFailedRequest( request );
                this.auth.refreshToken().subscribe( resp => {
                    if ( !resp ) {
                        console.log( "Invalid" );
                    } else {
                        this.auth.retryFailedRequests();
                    }
                } );

            }
        }
    } );

authentication.service.ts

cachedRequests: Array<HttpRequest<any>> = [];

public collectFailedRequest ( request ): void {
    this.cachedRequests.push( request );
}

public retryFailedRequests (): void {
    // retry the requests. this method can
    // be called after the token is refreshed
    this.cachedRequests.forEach( request => {
        request = request.clone( {
            setHeaders: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
                Authorization: `Bearer ${ this.getToken() }`
            }
        } );
        //??What to do here
    } );
}

上記のretryFailedRequests()ファイルは私が理解できないものです。再試行後にリクエストを再送信し、リゾルバーを介してルートで使用できるようにするにはどうすればよいですか?

それが役立つ場合、これはすべての関連コードです:https//gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9


3
私も同じ問題を抱えていますが、答えがないようです。
LastTribunal 2017

回答:


98

私の最終的な解決策。並列リクエストで機能します。

更新:コードはAngular 9 / RxJS 6で更新され、refreshTokenが失敗した場合のエラー処理とループの修正

import { HttpRequest, HttpHandler, HttpInterceptor, HTTP_INTERCEPTORS } from "@angular/common/http";
import { Injector } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, Observable, throwError } from "rxjs";
import { catchError, switchMap, tap} from "rxjs/operators";
import { AuthService } from "./auth.service";

export class AuthInterceptor implements HttpInterceptor {

    authService;
    refreshTokenInProgress = false;

    tokenRefreshedSource = new Subject();
    tokenRefreshed$ = this.tokenRefreshedSource.asObservable();

    constructor(private injector: Injector, private router: Router) {}

    addAuthHeader(request) {
        const authHeader = this.authService.getAuthorizationHeader();
        if (authHeader) {
            return request.clone({
                setHeaders: {
                    "Authorization": authHeader
                }
            });
        }
        return request;
    }

    refreshToken(): Observable<any> {
        if (this.refreshTokenInProgress) {
            return new Observable(observer => {
                this.tokenRefreshed$.subscribe(() => {
                    observer.next();
                    observer.complete();
                });
            });
        } else {
            this.refreshTokenInProgress = true;

            return this.authService.refreshToken().pipe(
                tap(() => {
                    this.refreshTokenInProgress = false;
                    this.tokenRefreshedSource.next();
                }),
                catchError(() => {
                    this.refreshTokenInProgress = false;
                    this.logout();
                }));
        }
    }

    logout() {
        this.authService.logout();
        this.router.navigate(["login"]);
    }

    handleResponseError(error, request?, next?) {
        // Business error
        if (error.status === 400) {
            // Show message
        }

        // Invalid token error
        else if (error.status === 401) {
            return this.refreshToken().pipe(
                switchMap(() => {
                    request = this.addAuthHeader(request);
                    return next.handle(request);
                }),
                catchError(e => {
                    if (e.status !== 401) {
                        return this.handleResponseError(e);
                    } else {
                        this.logout();
                    }
                }));
        }

        // Access denied error
        else if (error.status === 403) {
            // Show message
            // Logout
            this.logout();
        }

        // Server error
        else if (error.status === 500) {
            // Show message
        }

        // Maintenance error
        else if (error.status === 503) {
            // Show message
            // Redirect to the maintenance page
        }

        return throwError(error);
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
        this.authService = this.injector.get(AuthService);

        // Handle request
        request = this.addAuthHeader(request);

        // Handle response
        return next.handle(request).pipe(catchError(error => {
            return this.handleResponseError(error, request, next);
        }));
    }
}

export const AuthInterceptorProvider = {
    provide: HTTP_INTERCEPTORS,
    useClass: AuthInterceptor,
    multi: true
};

3
@ AndreiOstrovski、AuthServiceimportsのコードで答えを更新していただけませんか?
takeshin 2017

4
何らかの理由でthis.authService.refreshToken()が失敗した場合、更新を待機しているすべての並列クエリは永久に待機するように感じます。
Maksim Gumerov 2017

2
更新トークンのキャッチは、私を必要としません。Observable.throwにヒットしました。
jamesmpw

2
みんな、それは並列および順次のリクエストで動作します。5つのリクエストを送信すると、401が返され、1つのrefreshTokenが実行され、5つのリクエストが再度実行されます。5つのリクエストが連続している場合、最初の401の後、refreshTokenを送信し、次に最初のリクエストと他の4つのリクエストを送信します。
アンドレイオストロフスキー

2
Angularがサービスを装飾している場合にそれを実行できるのに、なぜ手動でサービスを注入するの@Injectable()ですか?また、1つのcatchErrorは何も返しません。少なくとも戻りEMPTYます。
Győriシャーンドル

16

Angular(7.0.0)とrxjs(6.3.3)の最新バージョンでは、これが完全に機能する自動セッション回復インターセプターを作成した方法です。同時リクエストが401で失敗した場合も、トークン更新APIにのみヒットする必要があります。一度失敗したリクエストを、switchMapとSubjectを使用してそのレスポンスにパイプします。以下は私のインターセプターコードがどのように見えるかです。authサービスとstoreサービスはかなり標準的なサービスクラスであるため、これらのコードは省略しました。

import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, Subject, throwError } from "rxjs";
import { catchError, switchMap } from "rxjs/operators";

import { AuthService } from "../auth/auth.service";
import { STATUS_CODE } from "../error-code";
import { UserSessionStoreService as StoreService } from "../store/user-session-store.service";

@Injectable()
export class SessionRecoveryInterceptor implements HttpInterceptor {
  constructor(
    private readonly store: StoreService,
    private readonly sessionService: AuthService
  ) {}

  private _refreshSubject: Subject<any> = new Subject<any>();

  private _ifTokenExpired() {
    this._refreshSubject.subscribe({
      complete: () => {
        this._refreshSubject = new Subject<any>();
      }
    });
    if (this._refreshSubject.observers.length === 1) {
      this.sessionService.refreshToken().subscribe(this._refreshSubject);
    }
    return this._refreshSubject;
  }

  private _checkTokenExpiryErr(error: HttpErrorResponse): boolean {
    return (
      error.status &&
      error.status === STATUS_CODE.UNAUTHORIZED &&
      error.error.message === "TokenExpired"
    );
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (req.url.endsWith("/logout") || req.url.endsWith("/token-refresh")) {
      return next.handle(req);
    } else {
      return next.handle(req).pipe(
        catchError((error, caught) => {
          if (error instanceof HttpErrorResponse) {
            if (this._checkTokenExpiryErr(error)) {
              return this._ifTokenExpired().pipe(
                switchMap(() => {
                  return next.handle(this.updateHeader(req));
                })
              );
            } else {
              return throwError(error);
            }
          }
          return caught;
        })
      );
    }
  }

  updateHeader(req) {
    const authToken = this.store.getAccessToken();
    req = req.clone({
      headers: req.headers.set("Authorization", `Bearer ${authToken}`)
    });
    return req;
  }
}

@ anton-toshikのコメントによると、このコードの機能を記事で説明するのは良い考えだと思いました。このコードの説明と理解については、こちらの私の記事を読んでください(どのようにそしてなぜそれが機能するのですか?)。それが役に立てば幸い。


1
よくreturnできました。intercept関数内の2番目は次のようになりますreturn next.handle(this.updateHeader(req)).pipe(。現在、あなただけ...それを更新した後にトークン認証を送る
malimo

私はスイッチマップを介してそれを行っていると思います。再度確認してください。私があなたの主張を誤解したかどうか教えてください。
サマルパン

はい、それは基本的に動作しますが、あなたは常に二回のリクエストを送信する-一度ヘッダなしで、そしてそれは、ヘッダーで失敗した後...
malimo

@SamarpanBhattacharyaこれは機能します。この答えは、Observableの動作を理解していない私のような誰かのためのセマンティクスの説明で行うことができると思います。
AntonToshik19年

1
@NikaKurashvili、このメソッドの定義は、私の仕事:public refreshToken(){const url:string=environment.apiUrl+API_ENDPOINTS.REFRESH_TOKEN;const req:any={token:this.getAuthToken()};const head={};const header={headers:newHttpHeaders(head)};return this.http.post(url,req,header).pipe(map(resp=>{const actualToken:string=resp['data'];if(actualToken){this.setLocalStorage('authToken',actualToken);}return resp;}));}
Shrinivas

9

私も同様の問題に遭遇し、収集/再試行ロジックが非常に複雑だと思います。代わりに、catch演算子を使用して401をチェックし、トークンの更新を監視して、リクエストを再実行できます。

return next.handle(this.applyCredentials(req))
  .catch((error, caught) => {
    if (!this.isAuthError(error)) {
      throw error;
    }
    return this.auth.refreshToken().first().flatMap((resp) => {
      if (!resp) {
        throw error;
      }
      return next.handle(this.applyCredentials(req));
    });
  }) as any;

..。

private isAuthError(error: any): boolean {
  return error instanceof HttpErrorResponse && error.status === 401;
}

1
カスタムステータスコード498を使用して、期限切れのトークンと401を識別します。これは、十分な特権がないことも示します
Joseph Carroll

1
こんにちは、return next.handle(reqClode)を使用しようとしていますが、何もしません。私のコードはあなたのビットとは異なりますが、機能しない部分はreturn部分です。authService.createToken(authToken、refreshToken); this.inflightAuthRequest = null; next.handle(req.clone({headers:req.headers.set(appGlobals.AUTH_TOKEN_KEY、authToken)}));を返します。

6
収集/再試行ロジックはそれほど複雑ではありません。トークンの有効期限が切れている間にrefreshTokenエンドポイントに複数のリクエストを送信したくない場合は、このロジックを実行する必要があります。トークンの有効期限が切れており、ほぼ同時に5つのリクエストを行ったとします。このコメントのロジックを使用すると、5つの新しい更新トークンがサーバー側で生成されます。
マリウスラザール

4
@JosephCarroll通常は十分ではない権限は403です
andrea.spot。

8

Andrei Ostrovskiの最終的な解決策は非常にうまく機能しますが、更新トークンも期限切れの場合は機能しません(更新するためのAPI呼び出しを行っていると仮定します)。少し掘り下げた後、refresh tokenAPI呼び出しもインターセプターによってインターセプトされていることに気付きました。これを処理するには、ifステートメントを追加する必要がありました。

 intercept( request: HttpRequest<any>, next: HttpHandler ):Observable<any> {
   this.authService = this.injector.get( AuthenticationService );
   request = this.addAuthHeader(request);

   return next.handle( request ).catch( error => {
     if ( error.status === 401 ) {

     // The refreshToken api failure is also caught so we need to handle it here
       if (error.url === environment.api_url + '/refresh') {
         this.refreshTokenHasFailed = true;
         this.authService.logout();
         return Observable.throw( error );
       }

       return this.refreshAccessToken()
         .switchMap( () => {
           request = this.addAuthHeader( request );
           return next.handle( request );
         })
         .catch((err) => {
           this.refreshTokenHasFailed = true;
           this.authService.logout();
           return Observable.throw( err );
         });
     }

     return Observable.throw( error );
   });
 }

refreshTokenHasFailedメンバーのブール値で他にどこで遊んでいるかを示していただけますか?
ステファン

1
上記のAndreiOstrovskiのソリューションで見つけることができます。基本的にはそれを使用しましたが、更新エンドポイントがインターセプトされたときに処理するifステートメントを追加しました。
James Lieu 2018年

これは意味がありません、なぜリフレッシュは401を返すのでしょうか?ポイントは、認証が失敗した後、それはリフレッシュを呼び出していますので、あなたのリフレッシュAPIがすべてで認証されてはならない、と401を返すべきではないということです
MDave

更新トークンには有効期限を設定できます。私たちのユースケースでは、4時間後に期限切れになるように設定されていました。ユーザーが一日の終わりにブラウザを閉じて翌朝戻った場合、更新トークンはその時点までに期限切れになるため、ログに記録する必要がありました。再び戻ってきます。更新トークンの有効期限が切れていない場合は、もちろん、このロジックを適用する必要はありません
James Lieu

4

この例に基づいて、これが私の作品です

@Injectable({
    providedIn: 'root'
})
export class AuthInterceptor implements HttpInterceptor {

    constructor(private loginService: LoginService) { }

    /**
     * Intercept request to authorize request with oauth service.
     * @param req original request
     * @param next next
     */
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
        const self = this;

        if (self.checkUrl(req)) {
            // Authorization handler observable
            const authHandle = defer(() => {
                // Add authorization to request
                const authorizedReq = req.clone({
                    headers: req.headers.set('Authorization', self.loginService.getAccessToken()
                });
                // Execute
                return next.handle(authorizedReq);
            });

            return authHandle.pipe(
                catchError((requestError, retryRequest) => {
                    if (requestError instanceof HttpErrorResponse && requestError.status === 401) {
                        if (self.loginService.isRememberMe()) {
                            // Authrozation failed, retry if user have `refresh_token` (remember me).
                            return from(self.loginService.refreshToken()).pipe(
                                catchError((refreshTokenError) => {
                                    // Refresh token failed, logout
                                    self.loginService.invalidateSession();
                                    // Emit UserSessionExpiredError
                                    return throwError(new UserSessionExpiredError('refresh_token failed'));
                                }),
                                mergeMap(() => retryRequest)
                            );
                        } else {
                            // Access token failed, logout
                            self.loginService.invalidateSession();
                            // Emit UserSessionExpiredError
                            return throwError(new UserSessionExpiredError('refresh_token failed')); 
                        }
                    } else {
                        // Re-throw response error
                        return throwError(requestError);
                    }
                })
            );
        } else {
            return next.handle(req);
        }
    }

    /**
     * Check if request is required authentication.
     * @param req request
     */
    private checkUrl(req: HttpRequest<any>) {
        // Your logic to check if the request need authorization.
        return true;
    }
}

ユーザーが有効になっているかどうかを確認することをお勧めします Remember Me更新トークンを使用して再試行できるかどうか、または単にログアウトページにリダイレクトます。

Fyi、にLoginServiceは次のメソッドがあります。
-getAccessToken():文字列-現在を返しますaccess_token
-isRememberMe():ブール値-ユーザーが持っているかどうかを確認しますrefresh_token
-refreshToken():Observable / Promise-oauthサーバーに新しいaccess_token使用を要求します-invalidateSession refresh_token
(): void-すべてのユーザー情報を削除し、ログアウトページにリダイレクトします


複数の更新リクエストを送信する複数のリクエストに問題がありますか?
CodingGorilla

私が最も好きなこのバージョンですが、私がリクエストを行うという問題があります。401が返されると更新が試行され、エラーが返されると継続的にリクエストの送信が試行され、停止することはありません。私は何か間違ったことをしていますか?
jamesmpw

申し訳ありませんが、前のものは慎重にテストしていませんでした。私が使用しているテスト済みの投稿で私の投稿を編集しました(rxjs6とrefeshトークンに移行し、URLを確認してください)。
Thanh Nhan 2018

1

理想的には、チェックしたい isTokenExpiredリクエストを送信前。また、有効期限が切れている場合は、トークンを更新し、ヘッダーに更新を追加します。

それ以外 retry operator、401応答でトークンを更新するロジックに役立つあります。

RxJS retry operatorリクエストを行うサービスでを使用します。それは受け入れますretryCount引数をます。指定しない場合、シーケンスを無期限に再試行します。

応答時にインターセプターでトークンを更新し、エラーを返します。サービスがエラーを返したが、現在は再試行演算子が使用されているため、リクエストを再試行し、今回は更新されたトークンを使用します(インターセプターは更新されたトークンを使用してヘッダーを追加します)。

import {HttpClient} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class YourService {

  constructor(private http: HttpClient) {}

  search(params: any) {
    let tryCount = 0;
    return this.http.post('https://abcdYourApiUrl.com/search', params)
      .retry(2);
  }
}

0
To support ES6 syntax the solution needs to be bit modify and that is as following also included te loader handler on multiple request


        private refreshTokenInProgress = false;
        private activeRequests = 0;
        private tokenRefreshedSource = new Subject();
        private tokenRefreshed$ = this.tokenRefreshedSource.asObservable();
        private subscribedObservable$: Subscription = new Subscription();



 intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (this.activeRequests === 0) {
            this.loaderService.loadLoader.next(true);
        }
        this.activeRequests++;

        // Handle request
        request = this.addAuthHeader(request);

        // NOTE: if the flag is true it will execute retry auth token mechanism ie. by using refresh token it will fetch new auth token and will retry failed api with new token
        if (environment.retryAuthTokenMechanism) {
            // Handle response
            return next.handle(request).pipe(
                catchError(error => {
                    if (this.authenticationService.refreshShouldHappen(error)) {
                        return this.refreshToken().pipe(
                            switchMap(() => {
                                request = this.addAuthHeader(request);
                                return next.handle(request);
                            }),
                            catchError(() => {
                                this.authenticationService.setInterruptedUrl(this.router.url);
                                this.logout();
                                return EMPTY;
                            })
                        );
                    }

                    return EMPTY;
                }),
                finalize(() => {
                    this.hideLoader();
                })
            );
        } else {
            return next.handle(request).pipe(
                catchError(() => {
                    this.logout();
                    return EMPTY;
                }),
                finalize(() => {
                    this.hideLoader();
                })
            );
        }
    }

    ngOnDestroy(): void {
        this.subscribedObservable$.unsubscribe();
    }

    /**
     * @description Hides loader when all request gets complete
     */
    private hideLoader() {
        this.activeRequests--;
        if (this.activeRequests === 0) {
            this.loaderService.loadLoader.next(false);
        }
    }

    /**
     * @description set new auth token by existing refresh token
     */
    private refreshToken() {
        if (this.refreshTokenInProgress) {
            return new Observable(observer => {
                this.subscribedObservable$.add(
                    this.tokenRefreshed$.subscribe(() => {
                        observer.next();
                        observer.complete();
                    })
                );
            });
        } else {
            this.refreshTokenInProgress = true;

            return this.authenticationService.getNewAccessTokenByRefreshToken().pipe(tap(newAuthToken => {
            this.authenticationService.updateAccessToken(newAuthToken.access_token);
            this.refreshTokenInProgress = false;
            this.tokenRefreshedSource.next();
        }));
        }
    }

    private addAuthHeader(request: HttpRequest<any>) {
        const accessToken = this.authenticationService.getAccessTokenOnly();
        return request.clone({
            setHeaders: {
                Authorization: `Bearer ${accessToken}`
            }
        });
    }

    /**
     * @todo move in common service or auth service once tested
     * logout and redirect to login
     */
    private logout() {
        this.authenticationService.removeSavedUserDetailsAndLogout();
    }

0

次の要件を解決する必要がありました。

  • ✅複数のリクエストに対してトークンを1回だけ更新する
  • ✅refreshTokenが失敗した場合はユーザーをログアウトします
  • ✅ユーザーが最初に更新した後にエラーが発生した場合はログアウトします
  • ✅トークンの更新中にすべてのリクエストをキューに入れる

その結果、Angularでトークンを更新するためにさまざまなオプションを収集しました。

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let retries = 0;
    return this.authService.token$.pipe(
      map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
      concatMap(authReq => next.handle(authReq)),
      // Catch the 401 and handle it by refreshing the token and restarting the chain
      // (where a new subscription to this.auth.token will get the latest token).
      catchError((err, restart) => {
        // If the request is unauthorized, try refreshing the token before restarting.
        if (err.status === 401 && retries === 0) {
          retries++;
    
          return concat(this.authService.refreshToken$, restart);
        }
    
        if (retries > 0) {
          this.authService.logout();
        }
    
        return throwError(err);
      })
    );
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.authService.token$.pipe(
      map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
      concatMap(authReq => next.handle(authReq)),
      retryWhen((errors: Observable<any>) => errors.pipe(
        mergeMap((error, index) => {
          // any other error than 401 with {error: 'invalid_grant'} should be ignored by this retryWhen
          if (error.status !== 401) {
            return throwError(error);
          }
    
          if (index === 0) {
            // first time execute refresh token logic...
            return this.authService.refreshToken$;
          }
    
          this.authService.logout();
          return throwError(error);
        }),
        take(2)
        // first request should refresh token and retry,
        // if there's still an error the second time is the last time and should navigate to login
      )),
    );
}

これらのオプションはすべて徹底的にテストされており、angular-refresh- tokengithubリポジトリにあります。


-3

失敗したリクエストのURLに基​​づいて新しいリクエストを作成し、失敗したリクエストの同じ本文を送信するようになりました。

 retryFailedRequests() {

this.auth.cachedRequests.forEach(request => {

  // get failed request body
  var payload = (request as any).payload;

  if (request.method == "POST") {
    this.service.post(request.url, payload).subscribe(
      then => {
        // request ok
      },
      error => {
        // error
      });

  }
  else if (request.method == "PUT") {

    this.service.put(request.url, payload).subscribe(
      then => {
       // request ok
      },
      error => {
        // error
      });
  }

  else if (request.method == "DELETE")

    this.service.delete(request.url, payload).subscribe(
      then => {
        // request ok
      },
      error => {
        // error
      });
});

this.auth.clearFailedRequests();        

}


-4

authentication.service.tsで、依存性としてHttpClientを注入する必要があります

constructor(private http: HttpClient) { }

次に、次のように(retryFailedRequests内で)リクエストを再送信できます。

this.http.request(request).subscribe((response) => {
    // You need to subscribe to observer in order to "retry" your request
});

これは私の最初の考えでしたが、http.requestはを返しますHttpEvent
Antoniossss
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.