Cześć, próbuję dowiedzieć się, jak zaimplementować nowe kątowe przechwytywacze i obsłużyć 401 unauthorized
błędy, odświeżając token i ponawiając żądanie. Oto przewodnik, który śledziłem: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors
Pomyślnie buforuję żądania, które nie powiodły się, i mogę odświeżyć token, ale nie mogę dowiedzieć się, jak ponownie wysłać żądania, które wcześniej zakończyły się niepowodzeniem. Chcę również, aby to działało z programami do rozwiązywania problemów, których obecnie używam.
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
} );
}
Powyższy plik retryFailedRequests () jest tym, czego nie mogę zrozumieć. Jak ponownie wysłać żądania i udostępnić je dla trasy przez program rozpoznawania nazw po ponownej próbie?
To jest cały odpowiedni kod, jeśli to pomaga: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9
źródło
Odpowiedzi:
Moje ostateczne rozwiązanie. Działa z równoległymi żądaniami.
AKTUALIZACJA: kod zaktualizowany za pomocą Angular 9 / RxJS 6, obsługa błędów i naprawienie zapętlenia w przypadku niepowodzenia 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 };
źródło
imports
i kodu usługi AuthService?@Injectable()
? Również jeden catchError nic nie zwraca. Przynajmniej wróćEMPTY
.W najnowszej wersji Angular (7.0.0) i rxjs (6.3.3) w ten sposób stworzyłem w pełni funkcjonalny przechwytywacz automatycznego odzyskiwania sesji, zapewniający, że jeśli jednoczesne żądania zawiodą z 401, to również powinno trafić tylko do interfejsu API odświeżania tokenu raz i potokuj nieudane żądania do odpowiedzi za pomocą switchMap i Subject. Poniżej jest jak wygląda mój kod przechwytujący. Pominąłem kod mojej usługi autoryzacji i usługi przechowywania, ponieważ są to dość standardowe klasy usług.
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; } }
Zgodnie z komentarzem @ anton-toshik, pomyślałem, że dobrym pomysłem jest wyjaśnienie działania tego kodu w artykule. Możesz przeczytać mój artykuł tutaj, aby uzyskać wyjaśnienie i zrozumienie tego kodu (jak i dlaczego działa?). Mam nadzieję, że to pomoże.
źródło
return
wewnątrzintercept
funkcji powinna wyglądać następująco:return next.handle(this.updateHeader(req)).pipe(
. Obecnie wysyłasz tokenpublic 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;}));}
Napotkałem również podobny problem i myślę, że logika zbierania / ponawiania jest zbyt skomplikowana. Zamiast tego możemy po prostu użyć operatora catch, aby sprawdzić 401, a następnie obserwować odświeżenie tokenu i ponownie uruchomić żądanie:
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; }
źródło
Ostateczne rozwiązanie Andrei Ostrovski działa naprawdę dobrze, ale nie działa, jeśli token odświeżania również wygasł (zakładając, że wykonujesz wywołanie interfejsu API w celu odświeżenia). Po pewnym czasie zdałem sobie sprawę, że wywołanie API tokena odświeżania zostało również przechwycone przez przechwytywacz. Musiałem dodać instrukcję if, aby sobie z tym poradzić.
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 ); }); }
źródło
refreshTokenHasFailed
członkiem boolean?Na podstawie tego przykładu , oto moja praca
@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; } }
Możesz chcieć sprawdzić, czy użytkownik ma włączoną opcję
Remember Me
odświeżania tokena do ponownej próby, czy po prostu przekierowuje do strony wylogowania.Fyi,
LoginService
ma następujące metody:- getAccessToken (): string - zwraca bieżący
access_token
- isRememberMe (): boolean - sprawdź, czy użytkownik ma
refresh_token
- refreshToken (): Observable / Promise - Żądanie do serwera oauth w celu nowego
access_token
użyciarefresh_token
- invalidateSession (): void - usuń wszystkie informacje o użytkowniku i przekieruj do strony wylogowania
źródło
Musiałem rozwiązać następujące wymagania:
W rezultacie zebrałem różne opcje, aby odświeżyć token w Angular:
tokenRefreshed$
BehaviourSubject jako semaforemcaught
parametru wcatchError
operatorze RxJS do ponowienia żądania nieudanego żądaniaintercept(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
operatora 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 )), ); }
Wszystkie te opcje są gruntownie przetestowane i można je znaleźć w repozytorium github angular -refresh-token
źródło
Najlepiej sprawdzić
isTokenExpired
przed wysłaniem zapytania. A jeśli wygasł, odśwież token i dodaj odświeżony w nagłówku.Poza tym
retry operator
może pomóc w logice odświeżania tokenu w odpowiedzi 401.Użyj
RxJS retry operator
w swojej usłudze, w której składasz żądanie. PrzyjmujeretryCount
argument. Jeśli nie zostanie podany, będzie ponawiać sekwencję w nieskończoność.W Twoim przechwytywaczu w odpowiedzi odśwież token i zwróć błąd. Gdy usługa zwróci błąd, ale teraz jest używany operator ponawiania, więc ponowi żądanie i tym razem z odświeżonym tokenem (przechwytujący używa odświeżonego tokenu do dodania w nagłówku).
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); } }
źródło
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(); }
źródło
Otrzymałem to, tworząc nowe żądanie na podstawie adresu URL nieudanego żądania i wysyłając tę samą treść nieudanego żądania.
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();
}
źródło
W pliku authentication.service.ts, jako zależność należy wstrzyknąć HttpClient
constructor(private http: HttpClient) { }
Następnie możesz ponownie przesłać żądanie (inside retryFailedRequests) w następujący sposób:
this.http.request(request).subscribe((response) => { // You need to subscribe to observer in order to "retry" your request });
źródło
HttpEvent
.