example javascript angular rxjs

javascript - interceptor angular 4 example



Solicitudes de reintento de Angular 4 Interceptor despuĆ©s de la actualizaciĆ³n del token (8)

Hola, estoy tratando de descubrir cómo implementar los nuevos interceptores angulares y manejar los errores 401 unauthorized al actualizar el token y volver a intentar la solicitud. Esta es la guía que he estado siguiendo: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors

Estoy almacenando en caché con éxito las solicitudes fallidas y puedo actualizar el token, pero no puedo entender cómo reenviar las solicitudes que fallaron anteriormente. También quiero que esto funcione con los resolvers que estoy usando actualmente.

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

autenticación.servicio.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 } ); }

El archivo retryFailedRequests () anterior es lo que no puedo entender. ¿Cómo vuelvo a enviar las solicitudes y las pongo a disposición de la ruta a través del solucionador después de volver a intentarlo?

Este es todo el código relevante si eso ayuda: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9


Basado en este ejemplo , aquí está mi pieza

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

Es posible que desee verificar si el usuario habilitó Remember Me para usar el token de actualización para volver a intentarlo o simplemente redirigir a la página de cierre de sesión.

Fyi, el LoginService tiene los siguientes métodos:
- getAccessToken (): string - devuelve el access_token actual
- isRememberMe (): boolean - verifica si el usuario tiene refresh_token
- refreshToken (): Observable / Promise - Solicitud de oauth server para el nuevo access_token usando refresh_token
- invalidateSession (): void - elimina toda la información del usuario y redirige a la página de cierre de sesión


Con la última versión de Angular (7.0.0) y rxjs (6.3.3), así es como creé un interceptor de recuperación de sesión automática completamente funcional que garantiza que, si las solicitudes concurrentes fallan con 401, también, solo debería golpear la API de actualización de token una vez y canalice las solicitudes fallidas a la respuesta de eso usando switchMap y Subject. A continuación se muestra cómo se ve mi código interceptor. He omitido el código para mi servicio de autenticación y servicio de tienda, ya que son clases de servicio bastante estándar.

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

Según el comentario de @ anton-toshik, pensé que es una buena idea explicar el funcionamiento de este código en una redacción. Puede leer mi artículo here para obtener la explicación y la comprensión de este código (¿cómo y por qué funciona?). Espero eso ayude.


En su autenticación.service.ts, debe tener un HttpClient inyectado como dependencia

constructor(private http: HttpClient) { }

Luego puede volver a enviar la solicitud (dentro de retryFailedRequests) de la siguiente manera:

this.http.request(request).subscribe((response) => { // You need to subscribe to observer in order to "retry" your request });


Idealmente, desea verificar isTokenExpired antes de enviar la solicitud. Y si expiró, actualice el token y agregue renovado en el encabezado.

Además de ese retry operator puede ayudarlo con su lógica de actualizar el token en la respuesta 401.

Utilice el RxJS retry operator en su servicio cuando realiza una solicitud. Acepta un argumento retryCount . Si no se proporciona, volverá a intentar la secuencia indefinidamente.

En su interceptor en la respuesta, actualice el token y devuelva el error. Cuando su servicio recupera el error, pero ahora se está utilizando el operador de reintento, por lo que volverá a intentar la solicitud y esta vez con el token actualizado (Interceptor usa el token actualizado para agregar en el encabezado).

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


La solución final de Andrei Ostrovski funciona muy bien, pero no funciona si el token de actualización también caducó (suponiendo que esté haciendo una llamada de API para actualizar). Después de investigar un poco, me di cuenta de que la llamada API de token de actualización también fue interceptada por el interceptor. Tuve que agregar una declaración if para manejar esto.

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


Mi solución final Funciona con solicitudes paralelas.

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


Obtuve esto creando una nueva solicitud basada en la URL de la solicitud fallida y enviando el mismo cuerpo de la solicitud fallida.

retryFailedRequests() { this.auth.cachedRequests.forEach(request => { // get failed request body var payload = (request as any).payload; if (request.method == "POST") { this.service.post(request.url, payload).subscribe( then => { // request ok }, error => { // error }); } else if (request.method == "PUT") { this.service.put(request.url, payload).subscribe( then => { // request ok }, error => { // error }); } else if (request.method == "DELETE") this.service.delete(request.url, payload).subscribe( then => { // request ok }, error => { // error }); }); this.auth.clearFailedRequests();

}


También me encontré con un problema similar y creo que la lógica de recopilación / reintento es demasiado complicada. En cambio, podemos usar el operador catch para verificar el 401, luego observar la actualización del token y volver a ejecutar la solicitud:

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