Hallo, ich versuche herauszufinden, wie die neuen Winkelabfangjäger implementiert und 401 unauthorized
Fehler behandelt werden, indem das Token aktualisiert und die Anforderung erneut versucht wird. Dies ist der Leitfaden, dem ich gefolgt bin: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors
Ich kann die fehlgeschlagenen Anforderungen erfolgreich zwischenspeichern und kann das Token aktualisieren, aber ich kann nicht herausfinden, wie die zuvor fehlgeschlagenen Anforderungen erneut gesendet werden. Ich möchte auch, dass dies mit den Resolvern funktioniert, die ich derzeit verwende.
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
} );
}
Die obige Datei retryFailedRequests () kann ich nicht herausfinden. Wie sende ich die Anforderungen erneut und stelle sie nach dem erneuten Versuch der Route durch den Resolver zur Verfügung?
Dies ist der gesamte relevante Code, wenn dies hilfreich ist: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9
quelle
Antworten:
Meine endgültige Lösung. Funktioniert mit parallelen Anfragen.
UPDATE: Der mit Angular 9 / RxJS 6 aktualisierte Code, Fehlerbehandlung und Fix-Looping, wenn refreshToken fehlschlägt
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 };
quelle
imports
und den Code des AuthService aktualisieren?@Injectable()
? Auch ein catchError gibt nichts zurück. Zumindest zurückEMPTY
.Mit der neuesten Version von Angular (7.0.0) und rxjs (6.3.3) habe ich auf diese Weise einen voll funktionsfähigen Interceptor für die automatische Sitzungswiederherstellung erstellt, der sicherstellt, dass bei gleichzeitig fehlgeschlagenen Anforderungen mit 401 auch nur die Token-Aktualisierungs-API getroffen werden sollte einmal und leiten Sie die fehlgeschlagenen Anforderungen mit switchMap und Subject an die Antwort weiter. Unten sehen Sie, wie mein Interceptor-Code aussieht. Ich habe den Code für meinen Authentifizierungsdienst und meinen Speicherdienst weggelassen, da es sich um Standarddienstklassen handelt.
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; } }
Laut @ anton-toshik-Kommentar hielt ich es für eine gute Idee, die Funktionsweise dieses Codes in einem Artikel zu erläutern. Sie können meinen Artikel hier lesen diesen Code zu erklären und zu verstehen (wie und warum funktioniert er?). Ich hoffe es hilft.
quelle
return
in derintercept
Funktion sollte so aussehen :return next.handle(this.updateHeader(req)).pipe(
. Derzeit senden Sie das Authentifizierungstoken erst nach dem Aktualisieren ...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;}));}
Ich bin auch auf ein ähnliches Problem gestoßen und denke, dass die Logik zum Sammeln / Wiederholen zu kompliziert ist. Stattdessen können wir einfach den catch-Operator verwenden, um nach dem 401 zu suchen, dann auf die Token-Aktualisierung zu achten und die Anforderung erneut auszuführen:
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; }
quelle
Die endgültige Lösung von Andrei Ostrovski funktioniert sehr gut, funktioniert jedoch nicht, wenn das Aktualisierungstoken ebenfalls abgelaufen ist (vorausgesetzt, Sie führen einen API-Aufruf zum Aktualisieren durch). Nach einigem Graben stellte ich fest, dass der API-Aufruf des Aktualisierungstokens auch vom Interceptor abgefangen wurde. Ich musste eine if-Anweisung hinzufügen, um dies zu handhaben.
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 ); }); }
quelle
refreshTokenHasFailed
Booleschen Mitglied spielen?Anhand dieses Beispiels ist hier mein Stück
@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; } }
Möglicherweise möchten Sie überprüfen, ob der Benutzer aktiviert ist
Remember Me
Verwendung des Aktualisierungstokens für einen erneuten Versuch aktiviert hat oder nur zur Abmeldeseite umleitet.Zu Ihrer Information, das
LoginService
hat die folgenden Methoden:- getAccessToken (): string - return the current
access_token
- isRememberMe (): boolean - prüfe, ob der Benutzer
refresh_token
- refreshToken (): Observable / Promise - Request to oauth server for new
access_token
usingrefresh_token
- invalidateSession (): ungültig - Entfernen Sie alle Benutzerinformationen und leiten Sie zur Abmeldeseite weiter
quelle
Idealerweise möchten Sie überprüfen
isTokenExpired
bevor die Anfrage gesendet wird. Und wenn abgelaufen, aktualisieren Sie das Token und fügen Sie aktualisiert im Header hinzu.Davon abgesehen
retry operator
kann dies bei Ihrer Logik zum Aktualisieren des Tokens bei der 401-Antwort hilfreich sein.Verwenden Sie das
RxJS retry operator
in Ihrem Dienst, wo Sie eine Anfrage stellen. Es akzeptiert aretryCount
Argument. Wenn nicht angegeben, wird die Sequenz auf unbestimmte Zeit wiederholt.Aktualisieren Sie in Ihrem Interceptor bei Antwort das Token und geben Sie den Fehler zurück. Wenn Ihr Dienst den Fehler zurückerhält, aber jetzt ein Wiederholungsoperator verwendet wird, wird die Anforderung wiederholt, und diesmal mit dem aktualisierten Token (Interceptor verwendet ein aktualisiertes Token, um den Header hinzuzufügen.)
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); } }
quelle
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(); }
quelle
Ich musste folgende Anforderungen lösen:
Infolgedessen habe ich verschiedene Optionen gesammelt, um das Token in Angular zu aktualisieren:
tokenRefreshed$
BehaviorSubject als Semaphorcaught
Parameters imcatchError
RxJS-Operator zum erneuten Versuch einer fehlgeschlagenen Anforderungintercept(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
RxJS-Operatorsintercept(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 )), ); }
Alle diese Optionen wurden gründlich getestet und finden Sie im Angular-Refresh-Token-Github-Repo
quelle
Ich habe eine neue Anfrage basierend auf der URL der fehlgeschlagenen Anfrage erstellt und den gleichen Text der fehlgeschlagenen Anfrage gesendet.
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();
}}
quelle
In Ihrer authentication.service.ts sollte ein HttpClient als Abhängigkeit eingefügt werden
constructor(private http: HttpClient) { }
Sie können die Anforderung dann erneut (innerhalb von retryFailedRequests) wie folgt erneut senden:
this.http.request(request).subscribe((response) => { // You need to subscribe to observer in order to "retry" your request });
quelle
HttpEvent
.