使用Angular全局处理401s


90

在我的Angular 2项目中,我从返回Observable的服务进行API调用。然后,调用代码订阅了此可观察的代码。例如:

getCampaigns(): Observable<Campaign[]> {
    return this.http.get('/campaigns').map(res => res.json());
}

假设服务器返回401。如何全局捕获此错误并重定向到登录页面/组件?

谢谢。


这是我到目前为止的内容:

// boot.ts

import {Http, XHRBackend, RequestOptions} from 'angular2/http';
import {CustomHttp} from './customhttp';

bootstrap(AppComponent, [HTTP_PROVIDERS, ROUTER_PROVIDERS,
    new Provider(Http, {
        useFactory: (backend: XHRBackend, defaultOptions: RequestOptions) => new CustomHttp(backend, defaultOptions),
        deps: [XHRBackend, RequestOptions]
    })
]);

// customhttp.ts

import {Http, ConnectionBackend, Request, RequestOptions, RequestOptionsArgs, Response} from 'angular2/http';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class CustomHttp extends Http {
    constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
        super(backend, defaultOptions);
    }

    request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {

        console.log('request...');

        return super.request(url, options);        
    }

    get(url: string, options?: RequestOptionsArgs): Observable<Response> {

        console.log('get...');

        return super.get(url, options);
    }
}

我收到的错误消息是“ backend.createConnection不是函数”


1
我想这可以给你一个小指针
潘卡Parkar

Answers:


78

描述

我发现最好的解决方案是重写XHRBackendHTTP响应状态,401403导致特定操作。

如果您在Angular应用程序之外处理身份验证,则可以强制刷新当前页面,从而触发您的外部机制。我在下面的实现中详细介绍了该解决方案。

您还可以转发到应用程序内部的组件,这样就不会重新加载Angular应用程序。

实作

角度> 2.3.0

感谢@mrgoos,这是对angular 2.3.0+的简化解决方案,这是由于直接扩展了模块的angular 2.3.0中的错误修复(请参见问题https://github.com/angular/angular/issues/11606Http

import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';


@Injectable()
export class AuthenticatedHttpService extends Http {

  constructor(backend: XHRBackend, defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options).catch((error: Response) => {
            if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
                console.log('The authentication session expires or the user is not authorised. Force refresh of the current page.');
                window.location.href = window.location.href + '?' + new Date().getMilliseconds();
            }
            return Observable.throw(error);
        });
  }
}

现在,模块文件仅包含以下提供程序。

providers: [
    { provide: Http, useClass: AuthenticatedHttpService }
]

@mrgoos 在以下要点中详细介绍了使用路由器和外部身份验证服务的另一种解决方案。

Angular 2.3.0版

以下实现适用于Angular 2.2.x FINALRxJS 5.0.0-beta.12

如果返回HTTP代码401或403,它将重定向到当前页面(加上用于获取唯一URL并避免缓存的参数)。

import { Request, XHRBackend, BrowserXhr, ResponseOptions, XSRFStrategy, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

export class AuthenticationConnectionBackend extends XHRBackend {

    constructor(_browserXhr: BrowserXhr, _baseResponseOptions: ResponseOptions, _xsrfStrategy: XSRFStrategy) {
        super(_browserXhr, _baseResponseOptions, _xsrfStrategy);
    }

    createConnection(request: Request) {
        let xhrConnection = super.createConnection(request);
        xhrConnection.response = xhrConnection.response.catch((error: Response) => {
            if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
                console.log('The authentication session expires or the user is not authorised. Force refresh of the current page.');
                window.location.href = window.location.href + '?' + new Date().getMilliseconds();
            }
            return Observable.throw(error);
        });
        return xhrConnection;
    }

}

与以下模块文件。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpModule, XHRBackend } from '@angular/http';
import { AppComponent } from './app.component';
import { AuthenticationConnectionBackend } from './authenticated-connection.backend';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent,
    ],
    entryComponents: [AppComponent],
    imports: [
        BrowserModule,
        CommonModule,
        HttpModule,
    ],
    providers: [
        { provide: XHRBackend, useClass: AuthenticationConnectionBackend },
    ],
})
export class AppModule {
}

2
谢谢!我发现了问题...我错过了这一行,这就是为什么catch()找不到它的原因。(smh) import "rxjs/add/operator/catch";
hartpdx

1
是否可以使用路由器模块进行导航?
Yuanfei Zhu朱

1
与Auth Guard捆绑的绝佳解决方案!1. Auth Guard检查授权用户(例如,通过查看LocalStorage)。2.在401/403响应中,您清洁了Guard的授权用户(例如,通过删除LocalStorage中的核心响应参数)。3.在此早期阶段,您无法访问路由器以转发到登录页面,刷新同一页面将触发Guard检查,这将把您转发到登录屏幕(并有选择地保留您的初始URL,因此您可以认证成功后将被转发到请求的页面。
亚历克斯·克劳斯

1
嘿@NicolasHenneaux-为什么您认为覆盖优先于此http?我看到的唯一好处是,您可以简单地将其用作提供程序:{ provide: XHRBackend, useClass: AuthenticationConnectionBackend }而在覆盖Http时,您需要编写更尴尬的代码,例如useFactory,通过调用“ new”并发送特定参数来限制自己。WDYT?关于第二种
mrgoos 16/11/13

3
@Brett-我为它创建了一个要点,它将为您提供帮助:gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos

82

角度4.3+

HttpClient的引入带来了轻松拦截所有请求/响应的功能。HttpInterceptors的常规用法已被详细记录,请参阅基本用法以及如何提供拦截器。以下是可以处理401错误的HttpInterceptor的示例。

已针对RxJS 6+更新

import { Observable, throwError } from 'rxjs';
import { HttpErrorResponse, HttpEvent, HttpHandler,HttpInterceptor, HttpRequest } from '@angular/common/http';

import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status == 401) {
          // Handle 401 error
        } else {
          return throwError(err);
        }
      })
    );
  }

}

RxJS <6

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).do(event => {}, err => {
            if (err instanceof HttpErrorResponse && err.status == 401) {
                // handle 401 errors
            }
        });
    }
}

1
这还在为您工作吗?昨天它对我有用,但是在安装其他模块后,出现此错误:next.handle(...)
do

我认为应该将它用作http之类的扩展,几乎总是闻起来
kboom

1
不要忘记使用HTTP_INTERCEPTORS将其添加到您的提供商列表中。您可以在文档中
Bruno Peres

2
很好,但是Router在这里使用似乎无效。例如,我想在用户收到401-403时将其路由到登录页面,但this.router.navigate(['/login']对我不起作用。它什么也没做
CodyBugstein

如果获取“ .do不是函数”,请import 'rxjs/add/operator/do';在导入rxjs之后添加。
amoss

19

由于前端API的过期时间比牛奶更快,因此在Angular 6+和RxJS 5.5+中,您需要使用pipe

import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private router: Router) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status === 401) {
          this.router.navigate(['login'], { queryParams: { returnUrl: req.url } });
        }
        return throwError(err);
      })
    );
  }
}

Angular 7+和rxjs 6+的更新

import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/internal/operators';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private router: Router) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .pipe(
        catchError((err, caught: Observable<HttpEvent<any>>) => {
          if (err instanceof HttpErrorResponse && err.status == 401) {
            this.router.navigate(['login'], { queryParams: { returnUrl: request.url } });
            return of(err as any);
          }
          throw err;
        })
      );
  }
}

我得到error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'..pipe是在那里,当我删除.pipe
BlackICE

2
@BlackICE我想这肯定了我回答中的第一句话。我已经更新了最新版本的答案。
Saeb Amini

1
在您的NG7 +例子req其实就是request-编辑是小我做
asked_io

12

Observable你从每个请求的方法得到的是类型Observable<Response>。该Response对象具有一个status属性,该属性将401在服务器返回该代码的情况下保存。因此,您可能希望在映射或转换之前检索它。

如果要避免在每次调用时都执行此功能,则可能必须扩展Angular 2的Http类,并注入自己的实现,该实现会调用super常规Http功能的parent(),然后401在返回对象之前处理错误。

看到:

https://angular.io/docs/ts/latest/api/http/index/Response-class.html


因此,如果我扩展Http,那么我应该能够从Http内重定向到“登录”路由吗?
pbz

那是理论。您必须将路由器注入Http实现中。
兰利

谢谢你的帮助。我用示例代码更新了问题。我可能做错了事(是Angular的新手)。知道会是什么吗?谢谢。
PBZ

您正在使用默认的Http提供程序,您必须创建自己的提供程序,该提供程序可解析为类的实例而不是默认实例。请参阅:angular.io/docs/ts/latest/api/core/Provider-class.html
Langley,

1
@Langley,谢谢。你是正确的:订阅((结果)=> {},(误差)=> {的console.log(error.status);}的误差参数仍然类型响应的。
abedurftig

9

角度4.3+

完成吉尔伯特·阿里纳斯匕首的答案:

如果您需要拦截任何错误,对其进行处理,然后将其转发到链中(而不仅仅是添加副作用.do),则可以使用HttpClient及其拦截器来执行以下操作:

import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // install an error handler
        return next.handle(req).catch((err: HttpErrorResponse) => {
            console.log(err);
            if (err.error instanceof Error) {
                // A client-side or network error occurred. Handle it accordingly.
                console.log('An error occurred:', err.error.message);
            } else {
                // The backend returned an unsuccessful response code.
                // The response body may contain clues as to what went wrong,
                console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
            }

            return Observable.throw(new Error('Your custom error'));
        });
    }
}

9

为了避免由于将诸如“ Router”之类的服务注入到Http派生类中而导致的循环引用问题,必须使用后构造器Injector方法。以下代码是Http服务的有效实现,每次REST API返回“ Token_Expired”时,该服务就会重定向到“登录”路由。请注意,它可用作常规Http的替代,因此,不需要更改应用程序已存在的组件或服务中的任何内容。

app.module.ts

  providers: [  
    {provide: Http, useClass: ExtendedHttpService },
    AuthService,
    PartService,
    AuthGuard
  ],

扩展的http.service.ts

import { Injectable, Injector } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

@Injectable()
export class ExtendedHttpService extends Http {
    private router; 
    private authService;

  constructor(  backend: XHRBackend, defaultOptions: RequestOptions, private injector: Injector) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
 
    if (typeof url === 'string') {
      if (!options) {
        options = { headers: new Headers() };
      }
      this.setHeaders(options);
    } else {
      this.setHeaders(url);
    }
    console.log("url: " + JSON.stringify(url) +", Options:" + options);

    return super.request(url, options).catch(this.catchErrors());
  }

  private catchErrors() {

    return (res: Response) => {
        if (this.router == null) {
            this.router = this.injector.get(Router);
        }
        if (res.status === 401 || res.status === 403) {
            //handle authorization errors
            //in this example I am navigating to login.
            console.log("Error_Token_Expired: redirecting to login.");
            this.router.navigate(['signin']);
        }
        return Observable.throw(res);
    };
  }

  private setHeaders(objectToSetHeadersTo: Request | RequestOptionsArgs) {
      
      if (this.authService == null) {
            this.authService = this.injector.get(AuthService);
      }
    //add whatever header that you need to every request
    //in this example I could set the header token by using authService that I've created
     //objectToSetHeadersTo.headers.set('token', this.authService.getToken());
  }
}


8

在Angular> = 2.3.0中,您可以覆盖HTTP模块并注入服务。在版本2.3.0之前,由于核心错误,您无法使用注入的服务。

我创建了要点来说明它是如何完成的。


感谢您的整理。我在app.module.ts中遇到一个生成错误,提示“找不到名称'Http'”,因此导入,现在出现以下错误:“无法实例化循环依赖项!Http:在NgModule AppModule中”
Bryan

嘿@ Brett-您可以分享您的app.module代码吗?谢谢。
mrgoos '16

好像还可以 您可以将扩展的HTTP添加到要点吗?此外,您是否还导入HTTP其他地方?
mrgoos '16

抱歉耽搁了。我现在在Angular 2.4上并遇到相同的错误。我在几个文件中导入Http。这是我更新的要点:gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Bryan

同样的问题在这里...看来这个要点没有工作,所以也许我们应该这样标记它?
Tuthmosis

2

角度> 4.3:基本服务的ErrorHandler

protected handleError(err: HttpErrorResponse | any) {
    console.log('Error global service');
    console.log(err);
    let errorMessage: string = '';

    if (err.hasOwnProperty('status')) { // if error has status
        if (environment.httpErrors.hasOwnProperty(err.status)) {
            // predefined errors
            errorMessage = environment.httpErrors[err.status].msg; 
        } else {
            errorMessage = `Error status: ${err.status}`;
            if (err.hasOwnProperty('message')) {
                errorMessage += err.message;
            }
        }
     }

    if (errorMessage === '') {
        if (err.hasOwnProperty('error') && err.error.hasOwnProperty('message')) { 
            // if error has status
            errorMessage = `Error: ${err.error.message}`;
        }
     }

    // no errors, then is connection error
    if (errorMessage === '') errorMessage = environment.httpErrors[0].msg; 

    // this.snackBar.open(errorMessage, 'Close', { duration: 5000 }});
    console.error(errorMessage);
    return Observable.throw(errorMessage);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.