Angular 4 and OAuth - Intercept 401 responses, ref

2019-01-15 23:03发布

问题:

As the title says, I am working on an Angular 4 project with OAuth authentication.

Whenever an http request responds with status code 401 I am intercepting the request, renewing the access token and retrying the failed request.

When I receive a 401, the request is correctly intercepted and the access token gets refreshed just as it should. The failed request also gets executed again, but doesn't deliver it's response to the component any longer.

So the problem is that my component, who should observe on the request response complains with the log 'Error receiving the properties' for the view before the refresh of the token and the retry of the request have taken place.

My interceptor:

import { Injectable, Inject, Injector } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpResponse,
  HttpErrorResponse,
  HttpEvent,
  HttpInterceptor,
  HttpSentEvent,
  HttpHeaderResponse,
  HttpProgressEvent,
  HttpUserEvent
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { AuthService } from './auth.service';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { TokenManager } from '../../../util/TokenManager';
import { AuthUserResponse } from '../../../models/authUserResponse';
import 'rxjs/add/operator/switchMap';

@Injectable()
export class AuthTokenExpiredInterceptor implements HttpInterceptor {

  isRefreshingToken: boolean = false;
  tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);

  constructor( private injector: Injector, private tokenManager: TokenManager ) {}


  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
    return next.handle(this.addNewAccessTokenToHeaders(request, this.tokenManager.retrieveAccessToken()))
    .do((event: HttpEvent<any>) => {
      if (event instanceof HttpResponse) {
          console.log('processing response', event);
      }
      return event;
    },(err) => {
      if (err instanceof HttpErrorResponse) {
        if (err.status === 401) {
          console.log('Access_token possibly expired, trying to retrieve a new one!')

          return this.handle401Error(request, next);
        } else if (err.status === 400) {
          console.log('Refresh_token possibly expired, redirecting to login');

          return this.handle400Error(err);
        }
      } else {
        return Observable.throw(err);
      }
    });
  }

  handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.isRefreshingToken) {
      console.log('in if');
      this.isRefreshingToken = true;

      // Reset here so that the following requests wait until the token comes back from the refreshToken call.
      this.tokenSubject.next(null);

      console.log('About to call renewAccessToken');
      return this.injector.get(AuthService).renewAccessToken().subscribe((response) => {
        let newToken = response.access_token;

        if (newToken) {
          console.log('Got the new access_token!');
          this.tokenSubject.next(newToken);
          let requestToRetry = this.addNewAccessTokenToHeaders(request, newToken);
          console.log('The retried request header: ' + requestToRetry.headers.get("Authorization"));
          return next.handle(requestToRetry);
        } else {  // No token in response
          this.injector.get(AuthService).logout();
        }
      },
      (err) => {
        this.injector.get(AuthService).logout();
        return Observable.throw(err)
      },
      () => {
        console.log('handle401Error done');
        this.isRefreshingToken = false;
      })        
    } else {
      console.log('In else');
      return this.tokenSubject
      .filter(token => token != null)
      .take(1)
      .switchMap(token => {
        return next.handle(this.addNewAccessTokenToHeaders(request, token));
      });
    }
  }


    handle400Error(error: HttpErrorResponse) {
      if (error && error.status === 400 && error.error && error.error.error === 'invalid_grant') {
        this.injector.get(AuthService).logout();
      }

        return Observable.throw(error);
      }

    addNewAccessTokenToHeaders(req: HttpRequest<any>, token: string): HttpRequest<any> {
      console.log('Adding the access_token to the Authorization header');
      return req.clone({ setHeaders: {
        Authorization: 'Bearer ' + token
      }})
    }
  }

My Service returns an Observable

and my component:

ngOnInit(){
    this.getProperties();
  }

  getProperties() {
    this.propertyService.getProperties().subscribe(
      result => {
      this.properties = result;
      console.log('Received response in Properties component: ' + JSON.stringify(result));
    }, error => {
      console.log('Error receiving the properties for the view')
    },
    () => { console.log('Received the properties, now they can be displayed in the view') })
  }

回答1:

Your function intercept must return always a Observable < HttpEvent < any > >. Your code is a bit "bizarro". The main problem I see is that you use "do" to catch the error. "do" not modify the request.

I have a intercept in this way (I hope the code can help you)

constructor(private inj: Injector) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    //if the request has "Authorization" we return the request
    if (req.headers.has('Authorization'))
      return next.handle(req);

    //I get here the AuthService
    const auth = this.inj.get(AuthService);

    //create the httpHeaders
    const httpHeaders = new HttpHeaders()
      .set('Content-Type', 'application/json; charset=utf-8')
      .set('Authorization', '' + auth.SID) //<-- I use auth.SID

    const authReq = req.clone({ headers: httpHeaders });

    return next.handle(authReq).catch((err: any) => { //<--if error use a catch
      if (err instanceof HttpErrorResponse) {
        if (err.status === 401) {
          //auth.recoverSID return a Observable<{value:new SID}>
          //use switchMap to really return next.handle(authReq)
          return auth.recoverSID().switchMap((value: IResponse) => {
            let httpHeaders = new HttpHeaders()
              .set('Content-Type', 'application/json; charset=utf-8')
              .set('Authorization', '' + value.SID)

            const authReq = req.clone({ headers: httpHeaders });
            return next.handle(authReq);
          })
        };
      }
      //Other case throw an error
      return Observable.throw(err);
    });
  }