Angular 2: How to handle http oauth authtoken 401 errors on a global basis

谁说胖子不能爱 提交于 2019-12-24 08:57:49

问题


I want to use the global error handler to handle access tokens expiring and refreshing them using the refresh token. My solution that almost seems to work is as follows:

rom your component, subscribe to an observable as follows:

   this.myHttpService.getSomeData().subscribe(response => {
               console.log('user logged in', response );   
      },
      error => {
        // optionally do something custom here.
        console.log(error);
        throw error;
        // we could handle the error here, getting the new auth token and calling this again. 
    }

The error handler above is not necessary, because we can handle the error in the global error handler, but you may want to handle something else here.

In my http service, I have a function returning an observable like this:

  getSomeData(): Observable<any> {
    return this.http.get(this.API_URL_BASE + '/api/activities/getsomeData, this.httpHelperMethodsService.defaultOptions).map((response: Response) => {     
      return response.json();
    }).catch(this.handleError);
  }

The process to handle auth tokens expiring will be as follows: 1. catch the error 2. recognise that the auth token has expired 3. call to get a new auth token using the refresh token 4. re-run the source observable

In the service, implement the handle error function, which looks like this:

private handleError(error: any, originalObservable: any) {
    let errorHelper = { 'error': error, 'observable': originalObservable};
    return  Observable.throw(errorHelper);
}

You can then create a global error handler as follows:

@Injectable()
export class ApplicationErrorHandler extends ErrorHandler {

constructor(private injector: Injector) {
    super(false);
  }

  handleError(error: any): void {
     const refreshTokenService = this.injector.get(RefreshTokenService);
      ... 
      else if(error.statusText == "Unauthorized"){
          // check if token is expired. This will involve looking in local storage and comparing expiry time to current time.
          if(isTokenExpired){
              refreshTokenService.refreshToken(refreshToken).subscribe(response => {
                     // set refresh token
                      error.observable().subscribe(response => {
                        // return response.
                        }, error => {
                         // return error. 
                         })
               }
          }
     }
  }

}

Use an http-interceptor, which is configured as follows:

import {Injectable} from "@angular/core";
import { ConnectionBackend, RequestOptions, Request, RequestOptionsArgs, Response, Http, Headers} from "@angular/http";
import {Observable} from "rxjs/Rx";


@Injectable()
export class InterceptedHttp extends Http {

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

    request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
        return super.request(url, options);
    }

    get(url: string, options?: RequestOptionsArgs): Observable<Response> {
        return super.get(url, this.getRequestOptionArgs(options));
    }

    post(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
        return super.post(url, body, this.getRequestOptionArgs(options));
    }

    put(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
        return super.put(url, body, this.getRequestOptionArgs(options));
    }

    delete(url: string, options?: RequestOptionsArgs): Observable<Response> {
        return super.delete(url, this.getRequestOptionArgs(options));
    }

    private getRequestOptionArgs(options?: RequestOptionsArgs) : RequestOptionsArgs {
        return this.getOptionsWithAccessToken();
    }

    getOptionsWithAccessToken() {
        let accessToken = localStorage.getItem('access_token');
        let accessTokenJson = <any>JSON.parse(accessToken);
        let headers = new Headers();
        headers.append('Content-Type', 'application/json');
        headers.append('Accept', 'application/json');
        headers.append('Authorization', accessTokenJson);
        let options = new RequestOptions({ headers: headers });
        console.log('options updated', options);
        return options;
      }
}

The http interceptor however, is not called when subscribing again to the original observable, instead I just get another 401. I can see based on the log statementconsole.log('options updated', options); this interceptor is being called prior to all other http calls. It seems that the http interceptor does not run when you subscribe to the original observable, is this the case?

Is there a way to update the options on this observable to use the new auth token? Perhaps I can call one of the other methods on the observable instead of subscribe? Or is this not possible? The only alternative I can think of is to catch the error on every http call, but I have about a hundred of them, so would rather not do this.


回答1:


I upgraded from using Http to HttpClient. I then implemented the new httpInterceptor where it is possible to pass in an Injector to access other services and checked with each request to see if the token is expiring soon, refreshing it if this was the case, otherwise catching the error.

@Injectable()
export class MyInterceptor implements HttpInterceptor {
    API_URL_BASE;
    isCurrentlyRefreshing: boolean = false;

    constructor(private injector: Injector) {
        this.API_URL_BASE = myGlobals.API_GLOBAL_VAR;
    }

    intercept(
        req: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<any>> {

        if (!this.isCurrentlyRefreshing) {
            var isTokenRefreshRequired = this.checkIfRefreshRequired();
            if (isTokenRefreshRequired) {
                this.refreshToken().subscribe(response => {
                    this.updateDetailsAfterLogin(response);

                    const httpService = this.injector.get(HttpHelperMethodsService);
                    httpService.loginFromRefreshToken();
                    this.isCurrentlyRefreshing = false;
                    console.log('response', response);
                }, error => {
                    this.isCurrentlyRefreshing = false;
                });
            }
        }


        return next.handle(req).do(evt => {

        }, err => {
            if (err instanceof HttpErrorResponse && err.status == 401) {
               let toastr = this.injector.get(TOASTR_TOKEN);

                this.refreshToken().subscribe(response => {
                this.updateDetailsAfterLogin(response);
                const httpService = this.injector.get(HttpHelperMethodsService);
                httpService.loginFromRefreshToken();
                toastr.info('User session refreshed following timeout. Please retry.');
            }, error => {
                if(error.error){
                    if(error.error.error == "invalid_grant"){
                        toastr.error("session timed out");
                        var userService = this.injector.get(UserService);
                        var router = this.injector.get(Router);
                        userService.logout();
                        router.navigate(['/login']);
                    }
                }
            })
        }
    });
}

Whilst I found it possible to refire a put or post request from here, by re-sending req after refreshing the token, the response did not link back to the initial observer. I decided that it would be ok to display a toastr in these rare edge cases.




回答2:


// Worked with  angular 5.2
import {
  HttpEvent,
  HttpInterceptor,
  HttpHandler,enter code here
  HttpRequest,
  HttpResponse,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw';
import { catchError, tap } from 'rxjs/operators';
import 'rxjs/add/operator/do';
import { Router } from '@angular/router';
import { Injectable } from '@angular/core';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(public router: Router) {}
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      tap(
        event => {},
        error => {
          this.handleError(error);
        }
      )
    );
  }
  private handleError(error: Response) {
    if (error.status === 401) {
      this.router.navigate(['/']);
    }
    return Observable.throw(error);
  }
}


来源:https://stackoverflow.com/questions/46795106/angular-2-how-to-handle-http-oauth-authtoken-401-errors-on-a-global-basis

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!