web-dev-qa-db-fra.com

Angular 4 Réessayer les requêtes après l'actualisation du jeton

Bonjour, j'essaie de comprendre comment implémenter les nouveaux intercepteurs angular et gérer les erreurs 401 unauthorized en actualisant le jeton et en renouvelant la demande. Voici le guide que j'ai suivi: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors

Je réussis à mettre en cache les demandes ayant échoué et je peux actualiser le jeton, mais je ne sais pas comment renvoyer les demandes qui ont précédemment échoué. Je souhaite également que cela fonctionne avec les résolveurs que j'utilise actuellement.

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

Le fichier retryFailedRequests () ci-dessus est ce que je ne peux pas comprendre. Comment renvoyer les demandes et les rendre disponibles sur l'itinéraire via le résolveur après une nouvelle tentative?

Ceci est tout le code pertinent si cela aide: https://Gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9

62
Kovaci

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);
        });
    }
}
67
Andrei Ostrovski

Avec la dernière version de Angular (7.0.0) et de rxjs (6.3.3), c’est ainsi que j’ai créé un intercepteur de récupération de session automatique entièrement fonctionnel, en veillant à ce que, si les demandes simultanées échouent avec 401, ne devrait frapper qu'une seule fois l'API d'actualisation de jeton et diriger les requêtes ayant échoué vers la réponse de cette dernière à l'aide de switchMap et de Subject. Ci-dessous, voici à quoi ressemble mon code d'intercepteur. J'ai omis le code de mon service d'authentification et de mon service de stockage, car ce sont des classes de service plutôt standard.

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

Selon le commentaire de @ anton-toshik, j'ai pensé que c'était une bonne idée d'expliquer le fonctionnement de ce code dans un article. Vous pouvez avoir une lecture sur mon article ici pour l'explication et la compréhension de ce code (comment et pourquoi cela fonctionne?). J'espère que ça aide.

11
Samarpan

J'ai rencontré un problème similaire également et je pense que la logique de collecte/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;
}
9
rdukeshier

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

Basé sur cet exemple , voici ma pièce

@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 pour utiliser le jeton d'actualisation pour réessayer ou simplement pour rediriger vers la page de déconnexion.

Fyi, le LoginService a les méthodes suivantes:
- getAccessToken (): string - retourne le access_token actuel
- isRememberMe (): boolean - vérifie si l'utilisateur a refresh_token
- refreshToken (): Observable/Promise - Demande au serveur oauth pour le nouveau access_token à l'aide de refresh_token
- invalidateSession (): void - supprime toutes les informations utilisateur et redirige vers la page de déconnexion

3
Thanh Nhan

Idéalement, vous voulez vérifier isTokenExpired avant l'envoi de la demande. Et si expiré, actualisez le jeton et ajoutez rafraîchi dans l'en-tête.

En dehors de cela, retry operator peut vous aider dans votre logique d'actualisation de jeton sur la réponse 401.

Utilisez le RxJS retry operator de votre service pour lequel vous faites une demande. Il accepte un argument retryCount. S'il n'est pas fourni, il réessayera la séquence indéfiniment.

Dans votre intercepteur en réponse, actualisez le jeton et renvoyez l'erreur. Lorsque votre service récupère l'erreur, mais que l'opérateur tente de réessayer est utilisé, il réessayera la demande et cette fois avec le jeton actualisé (Interceptor utilise un jeton actualisé pour l'ajouter dans l'en-tête.)

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