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