[javascript] 토큰 새로 고침 후 Angular 4 인터셉터 재시도 요청

안녕하세요 저는 새로운 각도 인터셉터를 구현 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



답변

내 마지막 해결책. 병렬 요청과 함께 작동합니다.

업데이트 : 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
};


답변

최신 버전의 Angular (7.0.0) 및 rxjs (6.3.3)를 사용하여 동시 요청이 401과 함께 실패 할 경우 토큰 새로 고침 API 만 누르도록 보장하는 완전한 기능의 자동 세션 복구 인터셉터를 만들었습니다. 한 번 실패한 요청을 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 코멘트에 따르면이 코드의 기능을 글로 설명하는 것이 좋은 생각이라고 생각했습니다. 이 코드에 대한 설명과 이해를 위해 여기 내 기사를 읽을 수 있습니다 (어떻게 작동 하는가?). 도움이 되었기를 바랍니다.


답변

비슷한 문제가 발생했으며 수집 / 재시도 논리가 지나치게 복잡하다고 생각합니다. 대신 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;
}


답변

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


답변

이 예를 바탕으로 내 작품입니다.

@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재 시도에 새로 고침 토큰을 사용할 수 있는지 확인 하거나 로그 아웃 페이지로 리디렉션 할 수 있습니다.

참고로, LoginService다음 메소드가 있습니다.
-getAccessToken () : 문자열-현재 반환 access_token
-isRememberMe () : boolean-사용자가 있는지 확인-refreshToken refresh_token
() : Observable / Promise-새로운 access_token사용 을 위해 oauth 서버에 요청-invalidateSession refresh_token
() : void-모든 사용자 정보를 제거하고 로그 아웃 페이지로 리디렉션


답변

이상적으로는 isTokenExpired요청을 보내기 전에 확인하는 것이 좋습니다 . 만료 된 경우 토큰을 새로 고치고 헤더에 새로 고침을 추가하십시오.

그 외에는 retry operator401 응답에서 토큰을 새로 고치는 논리에 도움 이 될 수 있습니다.

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


답변

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