Hola, estoy tratando de averiguar cómo implementar los nuevos interceptores angulares y manejar los 401 unauthorized
errores actualizando el token y volviendo 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 averiguar cómo reenviar las solicitudes que fallaron anteriormente. También quiero que esto funcione con los solucionadores 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();
}
} );
}
}
} );
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
} );
}
El archivo retryFailedRequests () anterior es lo que no puedo entender. ¿Cómo reenvío 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
fuente
Respuestas:
Mi solución final. Funciona con solicitudes paralelas.
ACTUALIZACIÓN: el código actualizado con Angular 9 / RxJS 6, manejo de errores y corrección de bucles cuando falla 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 };
fuente
imports
y el código de AuthService?@Injectable()
? Además, un catchError no devuelve nada. Al menos regresaEMPTY
.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 simultáneas fallan con 401, también debería presionar la API de actualización del 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 de 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 un artículo. Puede leer mi artículo aquí para obtener una explicación y comprensión de este código (¿cómo y por qué funciona?). Espero eso ayude.
fuente
return
dentro de laintercept
función debería tener este aspecto:return next.handle(this.updateHeader(req)).pipe(
. Actualmente, solo envía el token de autenticación después de actualizarlo ...public refreshToken(){const url:string=environment.apiUrl+API_ENDPOINTS.REFRESH_TOKEN;const req:any={token:this.getAuthToken()};const head={};const header={headers:newHttpHeaders(head)};return this.http.post(url,req,header).pipe(map(resp=>{const actualToken:string=resp['data'];if(actualToken){this.setLocalStorage('authToken',actualToken);}return resp;}));}
También me encontré con un problema similar y creo que la lógica de recopilación / reintento es demasiado complicada. En su lugar, podemos usar el operador de captura 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; }
fuente
La solución final de Andrei Ostrovski funciona muy bien, pero no funciona si el token de actualización también está vencido (asumiendo que estás haciendo una llamada a la API para actualizar). Después de investigar un poco, me di cuenta de que el interceptor también interceptó la llamada a la API del token de actualización. 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 ); }); }
fuente
refreshTokenHasFailed
miembro booleano?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 está habilitado
Remember Me
para usar el token de actualización para reintentar o simplemente redirigir a la página de cierre de sesión.Para su información,
LoginService
tiene los siguientes métodos:- getAccessToken (): cadena - devolver el actual
access_token
- isRememberMe (): boolean - verificar si el usuario tiene
refresh_token
- refreshToken (): Observable / Promise - Solicitud al servidor oauth para un nuevo
access_token
usorefresh_token
- invalidateSession (): void: elimina toda la información del usuario y redirige a la página de cierre de sesión
fuente
Idealmente, desea verificar
isTokenExpired
antes de enviar la solicitud. Y si expiró, actualice el token y agregue actualizado en el encabezado.Aparte de eso,
retry operator
puede ayudar con su lógica de actualizar el token en la respuesta 401.Utilice el
RxJS retry operator
en su servicio donde realiza una solicitud. Acepta unretryCount
argumento. Si no se proporciona, volverá a intentar la secuencia de forma indefinida.En su interceptor en respuesta, actualice el token y devuelva el error. Cuando su servicio recupere 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); } }
fuente
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(); }
fuente
Tuve que resolver los siguientes requisitos:
Como resultado, he recopilado diferentes opciones para actualizar el token en Angular:
tokenRefreshed$
BehaviorSubject como semáforocaught
parámetro en elcatchError
operador RxJS para reintentar la solicitud fallidaintercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { let retries = 0; return this.authService.token$.pipe( map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })), concatMap(authReq => next.handle(authReq)), // Catch the 401 and handle it by refreshing the token and restarting the chain // (where a new subscription to this.auth.token will get the latest token). catchError((err, restart) => { // If the request is unauthorized, try refreshing the token before restarting. if (err.status === 401 && retries === 0) { retries++; return concat(this.authService.refreshToken$, restart); } if (retries > 0) { this.authService.logout(); } return throwError(err); }) ); }
retryWhen
operador RxJSintercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return this.authService.token$.pipe( map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })), concatMap(authReq => next.handle(authReq)), retryWhen((errors: Observable<any>) => errors.pipe( mergeMap((error, index) => { // any other error than 401 with {error: 'invalid_grant'} should be ignored by this retryWhen if (error.status !== 401) { return throwError(error); } if (index === 0) { // first time execute refresh token logic... return this.authService.refreshToken$; } this.authService.logout(); return throwError(error); }), take(2) // first request should refresh token and retry, // if there's still an error the second time is the last time and should navigate to login )), ); }
Todas estas opciones se han probado exhaustivamente y se pueden encontrar en el repositorio de github de token de actualización angular
fuente
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();
}
fuente
En su authentication.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 });
fuente
HttpEvent
.