Привет, я пытаюсь понять, как реализовать новые перехватчики angular и обрабатывать 401 unauthorized
ошибки, обновляя токен и повторяя запрос. Это руководство, которым я следовал: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors
Я успешно кэширую неудавшиеся запросы и могу обновить токен, но не могу понять, как повторно отправить запросы, которые ранее были неудачными. Я также хочу, чтобы это работало с резолверами, которые я сейчас использую.
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
} );
}
Вышеупомянутый файл retryFailedRequests () - это то, что я не могу понять. Как мне повторно отправить запросы и сделать их доступными для маршрута через преобразователь после повторной попытки?
Если это помогает, это весь соответствующий код: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9
источник
Ответы:
Мое окончательное решение. Работает с параллельными запросами.
ОБНОВЛЕНИЕ: код обновлен с помощью Angular 9 / RxJS 6, обработка ошибок и исправление цикла при сбое 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 };
источник
imports
и код AuthService?@Injectable()
? Также одна ошибка catchError ничего не возвращает. По крайней мере, вернисьEMPTY
.С последней версией Angular (7.0.0) и rxjs (6.3.3) я создал полностью функциональный перехватчик восстановления Auto Session, гарантирующий, что если параллельные запросы завершаются с ошибкой с 401, то он также должен попадать только в API обновления токенов. один раз и направьте неудавшиеся запросы в ответ с помощью switchMap и Subject. Вот как выглядит мой код перехватчика. Я пропустил код для своей службы аутентификации и службы хранилища, поскольку это довольно стандартные классы обслуживания.
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; } }
Согласно комментарию @ anton-toshik, я подумал, что было бы неплохо объяснить функционирование этого кода в описании. Вы можете прочитать в моей статье здесь объяснение и понимание этого кода (как и почему он работает?). Надеюсь, поможет.
источник
return
внутриintercept
функции должен выглядеть следующим образом :return next.handle(this.updateHeader(req)).pipe(
. В настоящее время вы отправляете токен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;}));}
Я также столкнулся с аналогичной проблемой, и я считаю, что логика сбора / повторной попытки слишком сложна. Вместо этого мы можем просто использовать оператор catch для проверки 401, затем проследить за обновлением токена и повторно запустить запрос:
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; }
источник
Окончательное решение Андрея Островского работает очень хорошо, но не работает, если срок действия токена обновления также истек (при условии, что вы выполняете вызов api для обновления). Покопавшись, я понял, что вызов API токена обновления также был перехвачен перехватчиком. Мне пришлось добавить оператор if, чтобы справиться с этим.
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 ); }); }
источник
refreshTokenHasFailed
логическим членом?На основе этого примера вот моя работа
@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; } }
Вы можете проверить, разрешено ли пользователю
Remember Me
использовать токен обновления для повторной попытки или просто перенаправить на страницу выхода.Fyi, у
LoginService
него есть следующие методы:- getAccessToken (): string - вернуть текущий
access_token
- isRememberMe (): boolean - проверить, есть ли у пользователя
refresh_token
- refreshToken (): Observable / Promise - запрос к серверу oauth для нового
access_token
использованияrefresh_token
- invalidateSession (): void - удалить всю информацию о пользователе и перенаправить на страницу выхода
источник
Мне нужно было решить следующие требования:
В результате я собрал разные варианты для обновления токена в Angular:
tokenRefreshed$
BehaviorSubject в качестве семафораcaught
параметра вcatchError
операторе RxJS для повторной попытки запроса неудавшегося запросаintercept(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
оператора 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 )), ); }
Все эти параметры тщательно протестированы и их можно найти в репозитории github angular-refresh-token.
источник
В идеале вы хотите проверить
isTokenExpired
перед отправкой запроса. А если истекло, обновите токен и добавьте в заголовок обновленное.Помимо этого,
retry operator
может помочь логика обновления токена при ответе 401.Используйте
RxJS retry operator
в своем сервисе, где вы делаете запрос. Он принимаетretryCount
аргумент. Если не указан, последовательность будет повторяться бесконечно.В вашем перехватчике при ответе обновите токен и верните ошибку. Когда ваша служба возвращает ошибку, но теперь используется оператор повтора, поэтому он повторит запрос, и на этот раз с обновленным токеном (Interceptor использует обновленный токен для добавления в заголовок).
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); } }
источник
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(); }
источник
Я получил это, создав новый запрос на основе URL-адреса неудавшегося запроса и отправив тот же текст неудавшегося запроса.
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();
}
источник
В вашем authentication.service.ts у вас должен быть HttpClient, введенный как зависимость
constructor(private http: HttpClient) { }
Затем вы можете повторно отправить запрос (внутри retryFailedRequests) следующим образом:
this.http.request(request).subscribe((response) => { // You need to subscribe to observer in order to "retry" your request });
источник
HttpEvent
.