在全球范围内使用角度401

在我的 Angulal2项目中,我从返回可观察值的服务中调用 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 不是一个函数”

62015 次浏览

The Observable you get from each request method is of type Observable<Response>. The Response object, has an status property which will hold the 401 IF the server returned that code. So you might want to retrieve that before mapping it or converting it.

If you want to avoid doing this functionality on each call you might have to extend Angular 2's Http class and inject your own implementation of it that calls the parent (super) for the regular Http functionality and then handle the 401 error before returning the object.

See:

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

Description

The best solution I have found is to override the XHRBackend such that the HTTP response status 401 and 403 leads to a particular action.

If you handle your authentication outside your Angular application you could force a refresh of the current page such that your external mechanism is triggered. I detail this solution in the implementation below.

You could also forward to a component inside your application such that your Angular application is not reloaded.

Implementation

Angular > 2.3.0

Thanks to @mrgoos, here is a simplified solution for angular 2.3.0+ due to a bug fix in angular 2.3.0 (see issue https://github.com/angular/angular/issues/11606) extending directly the Http module.

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);
});
}
}

The module file now only contains the following provider.

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

Another solution using Router and an external authentication service is detailed in the following gist by @mrgoos.

Angular pre-2.3.0

The following implementation works for Angular 2.2.x FINAL and RxJS 5.0.0-beta.12.

It redirects to the current page (plus a parameter to get a unique URL and avoid caching) if an HTTP code 401 or 403 is returned.

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;
}


}

with the following module file.

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 {
}

From Angular >= 2.3.0 you can override the HTTP module and inject your services. Before version 2.3.0, you couldn't use your injected services due to a core bug.

I've created a gist to show how it's done.

To avoid the cyclic referencing issue that is caused by having services like "Router" being injected into an Http derived class, one must use the post-constructor Injector method. The following code is a working implementation of an Http service that redirects to Login route each time a REST API returns "Token_Expired". Note that it can be used as a substitution to the regular Http and as such, doesn't require to change anything in your application's already existing components or services.

app.module.ts

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

extended-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());
}
}

Angular 4.3+

With the introduction of HttpClient came the ability to easily intercept all requests / responses. The general usage of HttpInterceptors is well documented, see the basic usage and how to provide the interceptor. Below is an example of an HttpInterceptor that can handle 401 errors.

Updated for 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
}
});
}
}

Angular 4.3+

To complete The Gilbert Arenas Dagger answer:

If what you need is intercept any error, apply a treatment to it and forward it down the chain (and not just add a side effect with .do), you can use HttpClient and its interceptors to do something of the kind:

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'));
});
}
}

Angular >4.3: ErrorHandler for the base service

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);
}

As frontend APIs expire faster than milk, with Angular 6+ and RxJS 5.5+, you need to use 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);
})
);
}
}

Update for Angular 7+ and 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;
})
);
}
}