Попытка повторить http-запрос после обновления токена с помощью перехватчика в angular 7

Я пытаюсь автоматизировать запросы токена обновления при получении ошибки 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;
  })
);

Возможный дубликат Повторные запросы Angular 4 Interceptor после обновления токена

massic80 17.06.2019 16:01
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Angular и React для вашего проекта веб-разработки?
Angular и React для вашего проекта веб-разработки?
Когда дело доходит до веб-разработки, выбор правильного front-end фреймворка имеет решающее значение. Angular и React - два самых популярных...
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Мы провели Twitter Space, обсудив несколько проблем, связанных с последними дополнениями в Angular. Также прошла Angular Tiny Conf с 25 докладами.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
Мое недавнее углубление в Angular
Мое недавнее углубление в Angular
Недавно я провел некоторое время, изучая фреймворк Angular, и я хотел поделиться своим опытом со всеми вами. Как человек, который любит глубоко...
Освоение Observables и Subjects в Rxjs:
Освоение Observables и Subjects в Rxjs:
Давайте начнем с основ и постепенно перейдем к более продвинутым концепциям в RxJS в Angular
12
1
11 147
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете сделать что-то вроде этого:

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 пропускается

user10955671 23.01.2019 13:59

@Daniel Даниэль, я обновил ответ, вы пытались вернуть новый наблюдаемый объект в subscribe, вместо этого вам следует использовать mergeMap/flatMap.

Florian 23.01.2019 14:17

@ Даниэль, так у тебя есть решение? Потому что это действительно не работает для меня. Я вижу, что switchMap/mergeMap/flatMap обновляет токен, а затем этот токен добавляется в request, однако он не вызывается, а просто пропускается.

Vlad 03.08.2019 21:01
Ответ принят как подходящий

Вы должны различать среди всех запросов. Например, вы не хотите перехватывать запрос входа в систему, а также запрос токена обновления. 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)? Без него не работает? Что это делает, если я правильно понимаю, в поток событий помещается ноль, но подписчики все равно игнорируют ноль, так какой в ​​этом смысл?

lonix 02.07.2019 20:00

@Ionix смотрите мой РЕДАКТИРОВАТЬ

J. S. 03.07.2019 11:24

Кажется, теперь я вижу — перехватчик — это синглтон, поэтому наблюдаемый — тоже синглтон. Это означает, что В СЛЕДУЮЩИЙ РАЗ происходит обновление, этот наблюдаемый по-прежнему будет содержать токен доступа с прошлого раза... который является токеном с истекшим сроком действия! Таким образом, вы помещаете null в поток событий, чтобы предотвратить эту ошибку — не для этого цикла обновления, а для СЛЕДУЮЩЕГО! Имеет ли это смысл??

lonix 03.07.2019 13:42

Основная причина в том, что вы часто запускаете несколько запросов параллельно. Первый задействует механизм обновления, но вы хотите, чтобы остальные запросы ждали новый токен. Они ждут здесь: return this.tokenSubject.pipe(filter(token => token != null), пока не сработает this.tokenSubject.next(newToken). Если вы не выдаете null, то filter(token => token != null) не будет останавливать другие запросы, и все они будут использовать старый токен из последнего обновления. На самом деле это не баг, а фича :-)

J. S. 03.07.2019 14:10

Я понял это по-другому - другие (параллельные) запросы ждут, потому что take(1) ждет первое ненулевое событие (токен), а затем завершает поток. В СЛЕДУЮЩИЙ раз, когда происходит обновление (скажем, через 20 минут), BehaviorSubject все еще содержит тот жетон, что и в прошлый раз, срок действия которого истек! Поэтому мы помещаем null в конец потока, чтобы предотвратить немедленное завершение следующего цикла обновления с просроченным токеном... Я думаю, мы говорим об одном и том же по-разному! Но я новичок в rxjs и не такой эксперт, как вы, поэтому спасибо за помощь... вы мне очень помогли!! :-)

lonix 03.07.2019 14:22

@Дж.С. При обновлении токена next.hande(request) пропускается. Я вижу в Dev Tools, как мой первоначальный запрос получил 401, затем немедленно обновляется токен, однако первоначальный запрос больше не вызывается. Как я могу это исправить?

Vlad 03.08.2019 21:06

я использовал ту же функцию, и я получаю refreshToken, и мой ранее неудачный запрос также возобновляется и получает данные, но почему-то angular не отображается после получения обновленных данных, что делать, пожалуйста, помогите, @JS.

Haritsinh Gohil 23.01.2020 14:44

@HaritsinhGohil кажется, что это как-то связано с вашим компонентом, а не с перехватчиком. Можете ли вы открыть новый вопрос и опубликовать свой код компонента?

J. S. 04.02.2020 16:42

если кто-то разместил новый вопрос, пожалуйста, прикрепите ссылку здесь. Что касается меня, я получаю новый токен обновления, но некоторые из ожидающих запросов все еще используют старый токен.

Code Name Jack 07.04.2020 10:57

Это то, что я так долго искал... Большое спасибо за ваш потрясающий ответ!!

Tom el Safadi 09.10.2020 07:20

Я продолжаю возвращаться к этому ответу каждый раз, когда мне нужна эта функциональность. Это стало как ссылка на меня!

Yazan Khalaileh 01.05.2021 20:24

Ниже приведен код для вызова токена обновления, а после получения токена обновления вызывает неудачные 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?

Mark 20.11.2019 20:52

Я адаптирую ваш код, так что это не совсем то же самое, что и та же концепция. Я запускаю два запроса одновременно. Повторяется только первый. вторая не удалась, но не повторялась. Любые советы?

Mark 20.11.2019 20:57

Привет, Марк, ты прав, я снова протестировал его в другой среде, где он не работал с несколькими API.

Pravin P Patil 26.11.2019 15:07

Я работаю над этим, планирую сохранить сбойный API, кроме API токена, и после получения токена повторю попытку для сбойного API.

Pravin P Patil 26.11.2019 15:08

Чтобы ответить на вопрос @Mark, мы можем проверить сбой API из-за 401 (неавторизация) и будем хранить эти запросы в массиве с помощью next(HttpHandler) после того, как API токена выполнит свою задачу, после чего мы можем вызвать сбойный API с обновленным JWT. Я надеюсь, что это поможет вам и другим.

Pravin P Patil 18.12.2019 09:24

Другие вопросы по теме