web-dev-qa-db-ja.com

Angular 4トークン更新後のインターセプター再試行要求

こんにちは、新しいangularインターセプターを実装し、トークンを更新してリクエストを再試行することで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

58
Kovaci

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

export class AuthInterceptor implements HttpInterceptor {

    authService;
    refreshTokenInProgress = false;

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

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

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

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

            return this.authService.refreshToken()
               .do(() => {
                    this.refreshTokenInProgress = false;
                    this.tokenRefreshedSource.next();
                });
        }
    }

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

    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).catch(error => {

            if (error.status === 401) {
                return this.refreshToken()
                    .switchMap(() => {
                        request = this.addAuthHeader(request);
                        return next.handle(request);
                    })
                    .catch(() => {
                        this.logout();
                        return Observable.empty();
                    });
            }

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

私も同様の問題に遭遇し、収集/再試行のロジックは過度に複雑だと思います。代わりに、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;
}
9
rdukeshier

Angular(7.0.0)およびrxjs(6.3.3)の最新バージョンでは、これにより、同時リクエストが401で失敗した場合に、完全に機能する自動セッション回復インターセプターが作成されます。トークンリフレッシュAPIを1回だけヒットし、switchMapとSubjectを使用して、失敗したリクエストをそのレスポンスにパイプする必要があります。以下は、インターセプターコードの外観です。認証サービスとストアサービスはかなり標準的なサービスクラスであるため、コードを省略しました。

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のコメントによると、このコードの機能を説明文で説明するのは良い考えだと思いました。このコードの説明と理解については、私の記事 here で読むことができます(どのように、なぜ機能するのか?)。それが役に立てば幸い。

8
Samarpan

Andrei Ostrovskiの最終的なソリューションは非常にうまく機能しますが、更新トークンの有効期限が切れている場合は機能しません(更新のためのapi呼び出しを行っていると仮定)。少し掘り下げた後、インターセプターによってリフレッシュトークンAPI呼び出しもインターセプトされたことに気付きました。これを処理するには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 );
   });
 }
6
James Lieu

この例 に基づいて、ここに私の作品があります

@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():string-現在のaccess_tokenを返します
-isRememberMe():boolean-ユーザーがrefresh_tokenを持っているかどうかを確認します
-refreshToken():Observable/Promise-access_tokenを使用して、新しいrefresh_tokenのoauthサーバーへの要求
-invalidateSession():void-すべてのユーザー情報を削除し、ログアウトページにリダイレクトします

3
Thanh Nhan

理想的には、リクエストを送信する前に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
Lahar Shah