Angular 4 - перехватить HTTP-запрос и повторно отправить его после входа в систему

У меня есть HttpInterceptor, который прослушивает определенные события токена JWT (token_expired, token_not_provided и token_invalid), которые могут происходить в разное время рабочего процесса.

Эти события могут происходить, когда пользователь переходит на другой маршрут ИЛИ когда запрос AJAX отправляется по тому же маршруту (например, получение данных, сохранение формы и т. Д.).

Когда перехватчик обнаруживает любое из этих конкретных событий, он предлагает пользователю снова ввести учетные данные для входа (с использованием модального окна) и ставит запрос в очередь для последующей обработки (после того, как пользователь снова вошел в систему). Это важно, поскольку отправленные данные не могут быть потеряны (например, при обновлении заказа или клиента).

Упрощенная версия моего кода перехватчика:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private injector: Injector) {}
    router: Router;
    auth: AuthService;
    api: APIService;
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        this.router = this.injector.get(Router);
        this.auth = this.injector.get(AuthService);
        let token = this.auth.getToken();
        let headers = {
            'Content-Type':'application/json',
        };
        if (token) {
            (<any>headers).Authorization =  `Bearer ${token}`;
        }

        request = request.clone({
            setHeaders: headers
        });
        return next.handle(request).do((event: HttpEvent<any>) => {

        }, (err: any) => {
            if (err instanceof HttpErrorResponse) {
                let msg = typeof(err.error) === 'string' ? JSON.parse(err.error) : err.error;
                if (msg && msg.error && ['token_not_provided', 'token_expired','token_invalid'].indexOf(msg.error) > -1) {
                        this.auth.queueFailedRequest(request);
                        //set the intended route to current route so that after login the user will be shown the same screen
                        this.auth.setIntendedRoute(this.router.url);
                        //show the login popup
                        this.auth.promptLogin();
                    }
                }
            }
        });
    }
}

Соответствующая часть AuthService:

 queue: Array<HttpRequest<any>> = [];
 queueFailedRequest(request): void {
    this.queue.push(request);
 }

 retryFailedRequests(): void {
        this.queue.forEach(request => {
            this.retryRequest(request);
        });
        this.queue = [];
 }
 retryRequest(request): void {
        if (request.method === 'GET') {
             this.apiService.get(request.urlWithParams);
        }
        else if (request.method === 'POST') {
             this.apiService.post(request.urlWithParams, request.body || {});
        }
    }

И, конечно же, после успешного входа в систему я вызываю retryFailedRequests().

Пока все хорошо, и действительно, все HTTP-запросы ставятся в очередь и отправляются, если вход в систему прошел успешно.

А теперь к проблеме - если код структурирован как в этом примере (взят из компонента EditOrder):

updateOrder() {
   this.api.updateOrder(this.data).subscribe(res => {
     if (res.status === 'success') {
        alert('should be triggered even after login prompt');
     }
  });
}

Затем, если пользователю необходимо повторно войти в систему в процессе, предупреждение никогда не будет запущено после того, как метод retryFailedRequests() завершит обработку очереди.

Итак, вопрос в том, как лучше всего убедиться, что исходное обещание ставится в очередь вместе с HTTP-запросом и разрешается после завершения обработки очереди?


person dev7    schedule 03.10.2017    source источник
comment
Вы когда-нибудь придумали решение этой проблемы, Яни?   -  person binary lobster    schedule 06.03.2019


Ответы (1)


Итак, у меня была такая же проблема, и мы в конечном итоге решили ее, обернув Observable, который handle() возвращает, в наш собственный Observable, и вернув его из intercept().

В приведенном ниже примере this.requestQueue - это просто массив объектов, в котором мы можем сохранить подписчика из нового Observable, исходного HttpRequest и исходного HttpHandler. Мы делаем это для постановки запросов в очередь, если аутентификация еще не завершена.

Итак, вот метод intercept:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Return a new Observable
    return new Observable<HttpEvent<any>>((observer) => {
        if (this.authInProgress) {
            this.requestQueue.push({eventObserver: observer, request: req, handler: next});
        } else {
            this.processRequest(observer, req, next);
        }
    });

}

Поэтому тот, кто сделал первоначальный HTTP-запрос, должен подписаться на наблюдаемый объект, который не завершится, пока мы не скажем об этом, это очень важно. Ключевым моментом является то, что мы сохраняем ссылку на объект подписчика (observer), чтобы мы могли использовать его позже, чтобы получить ответы туда, где они принадлежат. Но мы фактически не обработали запрос в этом методе.

Вот processRequest():

private processRequest(eventObserver, request, handler) {
    // Handle the request
    // - pass the response along on success
    // - handle 401 errors and pass along all others
    handler.handle(request).subscribe(
        (event: HttpEvent<any>) => { eventObserver.next(event); },
        (err: any) => {
            if (err instanceof HttpErrorResponse && err.status === 401) {
                if (!this.authInProgress) {
                    // If this is the first 401 then we kick off some
                    // auth processes and mark authInProgress as true.
                    this.authInProgress = true;
                }

                // Save this request for later
                this.requestQueue.push({eventObserver, request, handler});
            } else {
                eventObserver.error(err);
            }
        },
        () => { eventObserver.complete(); }
    );
}

Итак, в processRequest() мы фактически отправляем запрос, вызывая handler.handle(request). Мы подписываемся сразу, и если запрос завершается успешно, мы отправляем событие ответа, вызывая eventObserver.next(event). Это отправит ответ тому, кто подписался на Observable, который мы вернули в intercept().

Если мы получим ошибку 401, мы просто сохраним запрос на потом, как мы это делали в intercept().

Позже, когда мы будем готовы обработать все эти запросы в очереди, мы pop() извлекаем их из requestQueue и передаем processRequest(). На этот раз, если ваша аутентификация сработала, они не будут ошибаться, и ответы об успешном завершении будут возвращены вышестоящему подписчику.

person binary lobster    schedule 27.03.2019