Я пытаюсь автоматизировать запросы токена обновления при получении ошибки 401 с угловым 7.
Между тем я не нахожу много документации о том, как это сделать с angular 7, и тем, что у меня нет предыдущих знаний об angular или rxjs, я становлюсь немного сумасшедшим.
Я думаю, что это почти завершено, но по какой-то причине второй next.handle(newReq) не отправляет запрос (в сетевом отладчике google chrome появляется только первый запрос)
я получаю ответ обновления и правильно делаю processLoginResponse (res)
вы можете увидеть здесь мой перехватчик
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let newReq = req.clone();
return next.handle(req).pipe(
catchError(error => {
if (error.status == 401) {
this._authenticationService.refresh().subscribe(
res => {
this._authenticationService.processLoginResponse(res);
newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
return next.handle(newReq)
},
error => {
this._authenticationService.logOut();
});
}
throw error;
})
);





Вы можете сделать что-то вроде этого:
import { HttpErrorResponse } from '@angular/common/http';
return next.handle(req).pipe(
catchError((err: any) => {
if (err instanceof HttpErrorResponse && err.status 401) {
return this._authenticationService.refresh()
.pipe(tap(
(success) => {},
(err) => {
this._authenticationService.logOut();
throw error;
}
).mergeMap((res) => {
this._authenticationService.processLoginResponse(res);
newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
return next.handle(newReq)
});
} else {
return Observable.of({});
}
}
));
та же проблема, второй возврат next.handle пропускается
@Daniel Даниэль, я обновил ответ, вы пытались вернуть новый наблюдаемый объект в subscribe, вместо этого вам следует использовать mergeMap/flatMap.
@ Даниэль, так у тебя есть решение? Потому что это действительно не работает для меня. Я вижу, что switchMap/mergeMap/flatMap обновляет токен, а затем этот токен добавляется в request, однако он не вызывается, а просто пропускается.
Вы должны различать среди всех запросов. Например, вы не хотите перехватывать запрос входа в систему, а также запрос токена обновления. SwitchMap — ваш лучший друг, потому что вам нужно отменить некоторые вызовы, чтобы дождаться обновления вашего токена.
Итак, что вы делаете, это сначала проверяете ответы об ошибках со статусом 401 (неавторизованный):
return next.handle(this.addToken(req, this.userService.getAccessToken()))
.pipe(catchError(err => {
if (err instanceof HttpErrorResponse) {
// token is expired refresh and try again
if (err.status === 401) {
return this.handleUnauthorized(req, next);
}
// default error handler
return this.handleError(err);
} else {
return observableThrowError(err);
}
}));
В вашей функции handleUnauthorized вы должны обновить свой токен, а также пропустить все дальнейшие запросы:
handleUnauthorized (req: HttpRequest<any>, next: HttpHandler): Observable<any> {
if (!this.isRefreshingToken) {
this.isRefreshingToken = true;
// Reset here so that the following requests wait until the token
// comes back from the refreshToken call.
this.tokenSubject.next(null);
// get a new token via userService.refreshToken
return this.userService.refreshToken()
.pipe(switchMap((newToken: string) => {
// did we get a new token retry previous request
if (newToken) {
this.tokenSubject.next(newToken);
return next.handle(this.addToken(req, newToken));
}
// If we don't get a new token, we are in trouble so logout.
this.userService.doLogout();
return observableThrowError('');
})
, catchError(error => {
// If there is an exception calling 'refreshToken', bad news so logout.
this.userService.doLogout();
return observableThrowError('');
})
, finalize(() => {
this.isRefreshingToken = false;
})
);
} else {
return this.tokenSubject
.pipe(
filter(token => token != null)
, take(1)
, switchMap(token => {
return next.handle(this.addToken(req, token));
})
);
}
}
У нас есть атрибут в классе перехватчика, который проверяет, выполняется ли уже запрос маркера обновления: this.isRefreshingToken = true;, потому что вы не хотите иметь несколько запросов на обновление при запуске нескольких несанкционированных запросов.
Таким образом, все в части if (!this.isRefreshingToken) касается обновления вашего токена и повторной попытки выполнить предыдущий запрос.
Все, что обрабатывается в else, предназначено для всех запросов, в то время как ваш userService обновляет токен, возвращается tokenSubject, и когда токен готов с this.tokenSubject.next(newToken);, каждый пропущенный запрос будет повторен.
Вот эта статья послужила источником вдохновения для создания перехватчика: https://www.intertech.com/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/
Обновлено:
TokenSubject на самом деле является Behavior Subject: tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);, что означает, что любой новый подписчик получит текущее значение в потоке, которое будет старым токеном с последнего вызова this.tokenSubject.next(newToken).
С next(null) каждый новый подписчик не запускает часть switchMap, поэтому filter(token => token != null) необходим.
После повторного вызова this.tokenSubject.next(newToken) с новым токеном каждый подписчик запускает часть switchMap с новым токеном. Надеюсь теперь понятнее
РЕДАКТИРОВАТЬ 21.09.2020
Исправить ссылку
Почему вы используете this.tokenSubject.next(null)? Без него не работает? Что это делает, если я правильно понимаю, в поток событий помещается ноль, но подписчики все равно игнорируют ноль, так какой в этом смысл?
@Ionix смотрите мой РЕДАКТИРОВАТЬ
Кажется, теперь я вижу — перехватчик — это синглтон, поэтому наблюдаемый — тоже синглтон. Это означает, что В СЛЕДУЮЩИЙ РАЗ происходит обновление, этот наблюдаемый по-прежнему будет содержать токен доступа с прошлого раза... который является токеном с истекшим сроком действия! Таким образом, вы помещаете null в поток событий, чтобы предотвратить эту ошибку — не для этого цикла обновления, а для СЛЕДУЮЩЕГО! Имеет ли это смысл??
Основная причина в том, что вы часто запускаете несколько запросов параллельно. Первый задействует механизм обновления, но вы хотите, чтобы остальные запросы ждали новый токен. Они ждут здесь: return this.tokenSubject.pipe(filter(token => token != null), пока не сработает this.tokenSubject.next(newToken). Если вы не выдаете null, то filter(token => token != null) не будет останавливать другие запросы, и все они будут использовать старый токен из последнего обновления. На самом деле это не баг, а фича :-)
Я понял это по-другому - другие (параллельные) запросы ждут, потому что take(1) ждет первое ненулевое событие (токен), а затем завершает поток. В СЛЕДУЮЩИЙ раз, когда происходит обновление (скажем, через 20 минут), BehaviorSubject все еще содержит тот жетон, что и в прошлый раз, срок действия которого истек! Поэтому мы помещаем null в конец потока, чтобы предотвратить немедленное завершение следующего цикла обновления с просроченным токеном... Я думаю, мы говорим об одном и том же по-разному! Но я новичок в rxjs и не такой эксперт, как вы, поэтому спасибо за помощь... вы мне очень помогли!! :-)
@Дж.С. При обновлении токена next.hande(request) пропускается. Я вижу в Dev Tools, как мой первоначальный запрос получил 401, затем немедленно обновляется токен, однако первоначальный запрос больше не вызывается. Как я могу это исправить?
я использовал ту же функцию, и я получаю refreshToken, и мой ранее неудачный запрос также возобновляется и получает данные, но почему-то angular не отображается после получения обновленных данных, что делать, пожалуйста, помогите, @JS.
@HaritsinhGohil кажется, что это как-то связано с вашим компонентом, а не с перехватчиком. Можете ли вы открыть новый вопрос и опубликовать свой код компонента?
если кто-то разместил новый вопрос, пожалуйста, прикрепите ссылку здесь. Что касается меня, я получаю новый токен обновления, но некоторые из ожидающих запросов все еще используют старый токен.
Это то, что я так долго искал... Большое спасибо за ваш потрясающий ответ!!
Я продолжаю возвращаться к этому ответу каждый раз, когда мне нужна эта функциональность. Это стало как ссылка на меня!
Ниже приведен код для вызова токена обновления, а после получения токена обновления вызывает неудачные API,
Комментарии в исходном коде помогут вам понять поток. Он протестирован и подходит для следующих сценариев.
1) If single request fails due to 401 then it will called for refresh token and will call failed API with updated token.
2) If multiple requests fails due to 401 then it will called for refresh token and will call failed API with updated token.
3) It will not call token API repeatedly
Если кто-то все еще нашел новый сценарий, в котором этот код не работает, сообщите мне, чтобы я протестировал и обновил его соответствующим образом.
import { Injectable } from "@angular/core";
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse, HttpHeaders, HttpClient, HttpResponse } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import { throwError, BehaviorSubject } from 'rxjs';
import { catchError, switchMap, tap, filter, take, finalize } from 'rxjs/operators';
import { TOKENAPIURL } from 'src/environments/environment';
import { SessionService } from '../services/session.service';
import { AuthService } from '../services/auth.service';
/**
* @author Pravin P Patil
* @version 1.0
* @description Interceptor for handling requests which giving 401 unauthorized and will call for
* refresh token and if token received successfully it will call failed (401) api again for maintaining the application momentum
*/
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private http: HttpClient, private sessionService: SessionService, private authService: AuthService) { }
/**
*
* @param request HttpRequest
* @param next HttpHandler
* @description intercept method which calls every time before sending requst to server
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Taking an access token
const accessToken = sessionStorage.getItem('ACCESS_TOKEN');
// cloing a request and adding Authorization header with token
request = this.addToken(request, accessToken);
// sending request to server and checking for error with status 401 unauthorized
return next.handle(request).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
// calling refresh token api and if got success extracting token from response and calling failed api due to 401
return this.handle401Error(request, next);
} // If api not throwing 401 but gives an error throwing error
else {
return throwError(error);
}
}));
}
/**
*
* @param request HttpRequest<any>
* @param token token to in Authorization header
*/
private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: { 'Authorization': `Bearer ${token}` }
});
}
/**
* This method will called when any api fails due to 401 and calsl for refresh token
*/
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
// If Refresh token api is not already in progress
if (this.isRefreshing) {
// If refreshTokenInProgress is true, we will wait until refreshTokenSubject has a non-null value
// – which means the new token is ready and we can retry the request again
return this.refreshTokenSubject
.pipe(
filter(token => token != null),
take(1),
switchMap(jwt => {
return next.handle(this.addToken(request, jwt))
}));
} else {
// updating variable with api is in progress
this.isRefreshing = true;
// Set the refreshTokenSubject to null so that subsequent API calls will wait until the new token has been retrieved
this.refreshTokenSubject.next(null);
const refreshToken = sessionStorage.getItem('REFRESH_TOKEN');
// Token String for Refresh token OWIN Authentication
const tokenDataString = `refresh_token=${refreshToken}&grant_type=refresh_token`;
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
'X-Skip-Interceptor': ''
})
};
return this.http.post<any>(TOKENAPIURL, tokenDataString, httpOptions)
.pipe(switchMap((tokens) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(tokens.access_token);
// updating value of expires in variable
sessionStorage.setItem('ACCESS_TOKEN', tokens.access_token);
return next.handle(this.addToken(request, tokens.access_token));
}));
}
}
}
не могли бы вы уточнить, как это работает? В частности, в какой момент новый токен сохраняется в LocalStorage?
Я адаптирую ваш код, так что это не совсем то же самое, что и та же концепция. Я запускаю два запроса одновременно. Повторяется только первый. вторая не удалась, но не повторялась. Любые советы?
Привет, Марк, ты прав, я снова протестировал его в другой среде, где он не работал с несколькими API.
Я работаю над этим, планирую сохранить сбойный API, кроме API токена, и после получения токена повторю попытку для сбойного API.
Чтобы ответить на вопрос @Mark, мы можем проверить сбой API из-за 401 (неавторизация) и будем хранить эти запросы в массиве с помощью next(HttpHandler) после того, как API токена выполнит свою задачу, после чего мы можем вызвать сбойный API с обновленным JWT. Я надеюсь, что это поможет вам и другим.
Возможный дубликат Повторные запросы Angular 4 Interceptor после обновления токена