Angular 4 - 拦截http请求并在登录后重新发送

标签 angular observable angular-http-interceptors

我有一个 HttpInterceptor 监听特定的 JWT token 事件(token_expiredtoken_not_providedtoken_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()方法处理完队列,将永远不会触发警报。

所以问题是确保原始 promise 与 HTTP 请求一起排队并在队列完成处理时解决的最佳方法是什么?

最佳答案

所以我遇到了同样的问题,我们最终通过handle() 返回的 Observable 包装在我们自己的 Observable 中,并从 intercept().

在下面的示例中,this.requestQueue 只是一个对象数组,我们可以在其中存储来自新 Observable 的订阅者、原始 HttpRequest 和原始 HttpHandler。如果身份验证尚未完成,我们这样做是为了对请求进行排队。

所以这是拦截方法:

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) 发送响应事件。这会将响应发送回订阅我们在 intercept() 中返回的 Observable 的任何人。

如果我们收到一个错误并且是 401,我们只是保存请求以备后用,就像我们在 intercept() 中所做的那样。

稍后,当我们准备好处理所有这些排队的请求时,我们将它们从 requestQueuepop() 并将它们传递给 processRequest( )。这一次,假设您的身份验证有效,他们不会出错并且成功响应将返回给上游订阅者。

关于Angular 4 - 拦截http请求并在登录后重新发送,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46554408/

相关文章:

angular - 如何使控制台中显示的错误引用有 Angular cli项目中的 typescript 源映射?

angular - 绑定(bind) : Appending to href

angular - 为什么我们需要订阅者方法 :Angular2

node.js - 如何将 RxJs observable 的结果作为 Node.js 的剩余响应返回

javascript - 具有选择性 url 的 AngularJS http 拦截器

Angular 2 拦截器无法正常工作

angular - 使用 Angular 过滤过滤 Angular Material 表中的特定列?

json - 带有正文的 Angular 2 Http 服务 GET

javascript - Angular 5 Typescript 表示 webpack 失败,因为响应类型上不存在属性数据

Angular 库 : Separate HttpInterceptor