การจัดการ 401s ทั่วโลกด้วย Angular


92

ในโครงการ Angular 2 ของฉันฉันทำการเรียก API จากบริการที่ส่งคืน Observable จากนั้นรหัสการโทรจะสมัครรับข้อมูลที่สังเกตได้นี้ ตัวอย่างเช่น:

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

// กำหนดเองhttp.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
ฉันคิดว่าสิ่งนี้สามารถชี้
Pankaj Parkar

คำตอบ:


79

คำอธิบาย

ทางออกที่ดีที่สุดที่ฉันพบคือการลบล้างXHRBackendสถานะการตอบสนองของ HTTP 401และ403นำไปสู่การดำเนินการบางอย่าง

หากคุณจัดการการรับรองความถูกต้องภายนอกแอปพลิเคชัน Angular ของคุณคุณสามารถบังคับให้รีเฟรชเพจปัจจุบันเพื่อให้กลไกภายนอกของคุณถูกทริกเกอร์ ฉันให้รายละเอียดโซลูชันนี้ในการใช้งานด้านล่าง

คุณยังสามารถส่งต่อไปยังส่วนประกอบภายในแอปพลิเคชันของคุณเพื่อไม่ให้แอปพลิเคชัน Angular ของคุณโหลดซ้ำได้

การนำไปใช้

เชิงมุม> 2.3.0

ขอบคุณ @mrgoos นี่คือวิธีแก้ปัญหาที่เรียบง่ายสำหรับเชิงมุม 2.3.0+ เนื่องจากการแก้ไขข้อบกพร่องในเชิงมุม 2.3.0 (ดูปัญหาhttps://github.com/angular/angular/issues/11606 ) ที่ขยายHttpโมดูลโดยตรง

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

เชิงมุมก่อน 2.3.0

การดำเนินการดังต่อไปนี้การทำงานสำหรับและAngular 2.2.x FINALRxJS 5.0.0-beta.12

จะเปลี่ยนเส้นทางไปยังหน้าปัจจุบัน (บวกพารามิเตอร์เพื่อรับ URL ที่ไม่ซ้ำกันและหลีกเลี่ยงการแคช) หากส่งคืนรหัส HTTP 401 หรือ 403

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 (เช่นโดยการลบพารามิเตอร์ coresponding ใน LocalStorage) 3. ในช่วงแรกนี้คุณไม่สามารถเข้าถึงเราเตอร์เพื่อส่งต่อไปยังหน้าเข้าสู่ระบบได้การรีเฟรชหน้าเดียวกันนี้จะเรียกใช้การตรวจสอบยามซึ่งจะส่งต่อคุณไปยังหน้าจอเข้าสู่ระบบ (และเลือกที่จะรักษา URL เริ่มต้นของคุณดังนั้นคุณจะ จะถูกส่งต่อไปยังหน้าที่ร้องขอหลังจากการตรวจสอบสิทธิ์สำเร็จ)
Alex Klaus

1
เฮ้ @NicolasHenneaux - ทำไมถึงคิดว่าดีกว่าที่จะลบล้างhttp? ประโยชน์อย่างเดียวที่ฉันเห็นคือคุณสามารถวางมันเป็นผู้ให้บริการได้: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }ในขณะที่เมื่อแทนที่ Http คุณต้องเขียนโค้ดที่น่าอึดอัดใจมากขึ้นเช่นuseFactoryและ จำกัด ตัวเองด้วยการเรียก 'ใหม่' และส่งอาร์กิวเมนต์เฉพาะ WDYT? อ้างอิงวิธีที่ 2: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos

3
@Brett - ฉันได้สร้างส่วนสำคัญสำหรับมันซึ่งน่าจะช่วยคุณได้: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos

84

เชิงมุม 4.3+

ด้วยการแนะนำHttpClientทำให้ความสามารถในการสกัดกั้นคำขอ / การตอบกลับทั้งหมดได้อย่างง่ายดาย การใช้งานทั่วไปของ HttpInterceptors ได้รับการบันทึกไว้เป็นอย่างดีดูการใช้งานพื้นฐานและวิธีการจัดหาเครื่องสกัดกั้น ด้านล่างนี้คือตัวอย่างของ HttpInterceptor ที่สามารถจัดการข้อผิดพลาด 401

อัปเดตสำหรับ 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 ไม่ใช่ฟังก์ชัน
Multitut

ฉันคิดว่าอันนี้ควรใช้เป็นส่วนขยายของคลาสเช่น http มักจะมีกลิ่น
kboom

1
อย่าลืมเพิ่มลงในรายชื่อผู้ให้บริการของคุณด้วย HTTP_INTERCEPTORS คุณสามารถดูตัวอย่างได้ในเอกสาร
Bruno Peres

2
เยี่ยมมาก แต่การใช้Routerที่นี่ดูเหมือนจะไม่ได้ผล ตัวอย่างเช่นฉันต้องการกำหนดเส้นทางผู้ใช้ของฉันไปยังหน้าเข้าสู่ระบบเมื่อพวกเขาได้รับ 401-403 แต่this.router.navigate(['/login']ไม่ได้ผลสำหรับฉัน ไม่มีอะไรเลย
CodyBugstein

หากคุณได้รับ ".do ไม่ใช่ฟังก์ชัน" ให้เพิ่มimport 'rxjs/add/operator/do';หลังจากที่คุณนำเข้า rxjs
amoss

20

เนื่องจาก 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 ฉันเดาว่าคงยืนยันประโยคแรกในคำตอบของฉัน ฉันได้อัปเดตพร้อมคำตอบสำหรับเวอร์ชันใหม่ล่าสุดแล้ว
แสบอามินี

1
ในตัวอย่าง ng7 + ของคุณreqเป็นจริงrequest- การแก้ไขมีขนาดเล็กสำหรับฉันที่จะทำ
ask_io

ทำไมเราใช้ interceprot และทำไมเราไม่สามารถจัดการได้โดยโทรเข้าสู่ระบบ api ด้วยวิธีการโพสต์
saber tabatabaee yazdi

12

คุณได้รับจากวิธีการร้องขอแต่ละประเภทObservable วัตถุมีคุณสมบัติที่จะถือถ้าเซิร์ฟเวอร์กลับรหัสที่ ดังนั้นคุณอาจต้องการดึงข้อมูลนั้นก่อนทำการแมปหรือแปลงObservable<Response>Responsestatus401

หากคุณต้องการหลีกเลี่ยงการใช้ฟังก์ชันนี้ในแต่ละการโทรคุณอาจต้องขยายHttpคลาสของ Angular 2 และฉีดการนำไปใช้งานของคุณเองที่เรียกพาเรนต์ ( super) สำหรับHttpฟังก์ชันการทำงานปกติจากนั้นจัดการ401ข้อผิดพลาดก่อนที่จะส่งคืนอ็อบเจ็กต์

ดู:

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


ดังนั้นหากฉันขยาย Http ฉันควรจะสามารถเปลี่ยนเส้นทางไปยังเส้นทาง "เข้าสู่ระบบ" จากภายใน Http?
pbz

นั่นคือทฤษฎี คุณจะต้องฉีดเราเตอร์ในการใช้งาน Http ของคุณถึงจะทำได้
Langley

ขอบคุณสำหรับความช่วยเหลือของคุณ. ฉันได้อัปเดตคำถามด้วยโค้ดตัวอย่าง ฉันอาจจะทำอะไรผิดพลาด (เป็นมือใหม่สำหรับ Angular) มีความคิดยังไงบ้าง? ขอบคุณ.
pbz

คุณกำลังใช้ผู้ให้บริการ Http เริ่มต้นคุณต้องสร้างผู้ให้บริการของคุณเองที่แก้ไขเป็นอินสแตนซ์ของชั้นเรียนของคุณแทนที่จะเป็นผู้ให้บริการเริ่มต้น ดู: angular.io/docs/ts/latest/api/core/Provider-class.html
Langley

1
@ Langley ขอบคุณ คุณพูดถูก: subscribe ((result) => {}, (error) => {console.log (error.status);} พารามิเตอร์ข้อผิดพลาดยังคงเป็นประเภทการตอบกลับ
abedurftig

9

เชิงมุม 4.3+

เพื่อให้คำตอบGilbert Arenas Dagger สมบูรณ์ :

หากสิ่งที่คุณต้องการคือสกัดกั้นข้อผิดพลาดใด ๆ ให้ใช้วิธีการรักษาและส่งต่อไปตามห่วงโซ่ (ไม่ใช่แค่เพิ่มผลข้างเคียงด้วย.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

เพื่อหลีกเลี่ยงปัญหาการอ้างอิงแบบวนรอบที่เกิดจากการมีเซอร์วิสเช่น "เราเตอร์" ที่ถูกแทรกลงในคลาสที่ได้รับ Http เราต้องใช้วิธีการหลังการสร้างตัวสร้างหัวฉีด รหัสต่อไปนี้เป็นการใช้งานบริการ 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 คุณไม่สามารถใช้บริการที่ฉีดเข้าไปเนื่องจากข้อบกพร่องหลัก

ฉันได้สร้างส่วนสำคัญเพื่อแสดงวิธีการทำ


ขอบคุณที่นำมารวมกัน ฉันได้รับข้อผิดพลาดในการสร้างที่ระบุว่า "ไม่พบชื่อ 'Http'" ใน app.module.ts ดังนั้นฉันจึงนำเข้าและตอนนี้ได้รับข้อผิดพลาดต่อไปนี้: "ไม่สามารถสร้างอินสแตนซ์การพึ่งพาแบบวนได้! http: ใน NgModule AppModule"
ไบรอัน

เฮ้ @ Brett- คุณแชร์app.moduleรหัสของคุณได้ไหม ขอบคุณ.
mrgoos

ดูเหมือนว่าตกลง คุณสามารถเพิ่มส่วนสำคัญ HTTP แบบขยายได้หรือไม่ นอกจากนี้คุณนำเข้าHTTPที่อื่นหรือไม่?
mrgoos

ขออภัยในความล่าช้า. ตอนนี้ฉันใช้ Angular 2.4 และได้รับข้อผิดพลาดเดียวกัน ฉันนำเข้า Http ในหลายไฟล์ นี่คือส่วนสำคัญที่อัปเดตของฉัน: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
ไบรอัน

ปัญหาเดียวกันที่นี่ ... ดูเหมือนว่าส่วนสำคัญนี้จะไม่ทำงานดังนั้นเราควรทำเครื่องหมายว่าเป็นเช่นนั้นหรือไม่
Tuthmosis

2

Angular> 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);
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.