113 votes

Requêtes de nouvelle tentative d'intercepteur angulaire 4 après l'actualisation du jeton

Salut, je suis à essayer de comprendre comment mettre en œuvre la nouvelle angulaire d'intercepteurs et de manipuler 401 unauthorized des erreurs par l'actualisation de la symbolique et de la nouvelle tentative de la demande. C'est le guide que j'ai suivi: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors

Je suis avec succès la mise en cache l'échec de la demande et peut actualiser le jeton mais je ne peux pas comprendre comment renvoyer les demandes que précédemment échoué. Je tiens également à obtenir que cela fonctionne avec les outils de résolution que j'utilise actuellement.

jeton.l'intercepteur.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();
                    }
                } );

            }
        }
    } );

l'authentification.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
    } );
}

Le ci-dessus retryFailedRequests() du fichier est ce que je ne peux pas comprendre. Comment puis-je renvoyer les demandes et de les rendre disponibles à la route par le résolveur après une nouvelle tentative?

C'est tout le code si ça peut aider: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9

136voto

Andrei Ostrovski Points 651

Ma solution finale Fonctionne avec des demandes parallèles.

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

19voto

Samarpan Points 483

Avec la dernière version de Angulaire (7.0.0) et rxjs (6.3.3), c'est la façon dont j'ai créé une application totalement fonctionnelle Automatique de récupération de la Session de l'intercepteur de s'assurer, si les demandes simultanées échouer avec l'erreur 401, puis aussi, il ne doit frapper jeton d'actualisation de l'API une fois et canaliser les demandes ayant échoué à la réponse que l'utilisation de switchMap et de l'Objet. Ci-dessous est comment mon intercepteur à quoi ressemble le code. J'ai omis le code de mon auth service et le service de banque comme ils sont assez standard classes de service.

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

Comme par @anton-toshik commentaire, j'ai pensé que c'est une bonne idée d'expliquer le fonctionnement de ce code dans une écriture-up. Vous pouvez lire mon article ici pour l'explication et de la compréhension de ce code (comment et pourquoi ça marche?). Espérons que cela aide.

11voto

SimplyJhay Points 11

La solution finale d'Andrei Ostrovski fonctionne très bien, mais ne fonctionne pas si le jeton d'actualisation est également expiré (en supposant que vous passiez un appel api pour l'actualisation). Après quelques recherches, j'ai réalisé que l'appel d'API de jeton d'actualisation était également intercepté par l'intercepteur. J'ai dû ajouter une instruction if pour gérer cela.

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

9voto

rdukeshier Points 99

J'ai rencontré un problème similaire également et je pense que la logique de collecte / nouvelle tentative est trop compliquée. Au lieu de cela, nous pouvons simplement utiliser l'opérateur de capture pour rechercher la valeur 401, puis surveiller l'actualisation du jeton et réexécuter la demande:

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

5voto

Thanh Nhan Points 111

Basé sur cet exemple, voici mon morceau

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

Vous voudrez peut-être vérifier si l'utilisateur a activé Remember Me de l'utilisation d'actualisation jeton pour une nouvelle tentative ou tout simplement rediriger à la page de déconnexion.

Pour info, l' LoginService a les méthodes suivantes:
- getAccessToken(): string - retour le courant access_token
- isRememberMe(): boolean - vérifier que l'utilisateur refresh_token
- refreshToken(): Observables / Promesse - Demande à oauth serveur pour la nouvelle - access_token l'aide refresh_token
- invalidateSession(): void - supprimer toutes les infos de l'utilisateur et le rediriger à la page de déconnexion

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X