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