Optionally apply http interceptors in angular 4

2019-06-21 00:28发布

问题:

The following url explained how to use http interceptor in Angular 4: https://angular.io/guide/http#intercepting-all-requests-or-responses

However, I'm wondering whether there is any way to choose whether to use the interceptors or not? Or to choose which set of interceptors to use? I want to implement a set of authentication interceptors, however, I don't want these interceptors to be applied when users access things like login, signup and forget-password which do not need user authentication.

All I need is to inject a clean instance of HttpClient into my service, if I need. I don't like the way that I can only use a single global instance of HttpClient that is being polluted by all interceptors.

回答1:

I had this very same requirement and came up with the following solution.

In the Module, I 'provide' an HttpClient using a Token as follows.


    export const HTTP_NOAUTH = new InjectionToken("http_noauth");
    ...
    providers: [...,
    {
        provide: HTTP_NOAUTH,
        deps: [HttpBackend],
        useFactory: (handler: HttpBackend) => {
            return new HttpClient(handler);
        }
    },
    {
        provide: HTTP_INTERCEPTORS,
        useClass: AuthHttpInterceptor,
        multi: true
    }],
    ...

Then, when I am injecting the HttpClient and don't want the AuthHttpInterceptor to be utilized, I specify '@Inject(HTTP_NOAUTH)'.


    @Injectable({
        providedIn: 'root',
    })
    export class SomeService {
        constructor(@Inject(HTTP_NOAUTH) private http: HttpClient) {
        ...

The one major hole in this that I've found so far (and there may be more) is that it's an all or nothing solution. It either has all the Interceptors or it has none of them. It may be possible to inject individual Interceptors in the Token provided entry but I haven't dug far enough yet.

UPDATE:

I can now select which interceptors to exclude for each configuration of an HttpClient as follows.

import { Observable } from 'rxjs';
import { HttpHandler, HttpEvent, HttpRequest, HttpInterceptor, HttpBackend, HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { Injector, InjectionToken } from '@angular/core';

export const provideTokenizedHttpClient = (token: InjectionToken<string>, options: { excludes: Function[] } = { excludes: [] }) => {
    return {
        provide: token,
        deps: [HttpBackend, Injector],
        useFactory: (backend: HttpBackend, injector: Injector) => {
            return new HttpClient(
                new HttpDynamicInterceptingHandler(backend, injector, options)
            );
        }
    }
}

class HttpInterceptorHandler implements HttpHandler {
    constructor(private next: HttpHandler, private interceptor: HttpInterceptor) { }
    handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
        return this.interceptor.intercept(req, this.next);
    }
}

class HttpDynamicInterceptingHandler implements HttpHandler {
    private chain: any = null;

    constructor(private backend: HttpBackend, private injector: Injector, private options: { excludes: Function[] } = { excludes: [] }) { }

    public handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
        if (this.chain === null) {
            const interceptors = this.injector.get(HTTP_INTERCEPTORS, [])
                .filter(entry => !this.options.excludes.includes(entry.constructor));

            this.chain = interceptors.reduceRight((next, interceptor) => {
                return new HttpInterceptorHandler(next, interceptor);
            }, this.backend);
        }
        return this.chain.handle(req);
    }
}

And now in my providers I simply use the following:

providers: [...
    provideTokenizedHttpClient(HTTP_NOAUTH, { excludes: [AuthHttpInterceptor] }),
    {
        provide: HTTP_INTERCEPTORS,
        useClass: AppBusyHttpInterceptor,
        multi: true
    },
    {
        provide: HTTP_INTERCEPTORS,
        useClass: AuthHttpInterceptor,
        multi: true
    }],

The creation of the InjectionToken and its usage on the @Inject decorator is the same.



回答2:

I had similar problem and I spend a lot of time to resolve it. I wanted to make interceptors layer independent from interceptors apply (or not apply) logic.

My solutions was to make separated module dedicated only for interceptors. None of interceptors includes if-conditions to decide if should fire or not. I moved this logic to the forRoot method, which is dedicated to pass additional configuration to the module.

Example call:

InterceptorModule.forRoot([
  {
    provide: HTTP_INTERCEPTORS,
    useClass: MenuInterceptor,
    multi: true,
    runConditions: [InterceptConditions.WhiteList],
    whiteList: ['api/menu']
  },
  {
    provide: HTTP_INTERCEPTORS,
    useClass: AuthorizationInterceptor,
    multi: true,
    runConditions: [InterceptConditions.BlackList],
    blackList: ['api/public']
  },
]),

It's all. Interceptors contains pure logic and forRoot method contains run conditions.

To make it work you have to:

  • Add additional abstract class with encapsulated logic to support black/white lists. Every interceptor has to extend it.
  • Overload original ClassProvider interface with additional fields

example:

export interface InterceptorClassProvider extends ClassProvider {
  runConditions: InterceptConditions[];
  whiteList?: string[];
  blackList?: string[];
}
  • load everyhing in forRoot method

example:

static forRoot(interceptorProviders: InterceptorClassProvider[]): ModuleWithProviders {
  return {
    ngModule: CwaCoreInterceptorModule,
    providers: [
      { provide: INTERCEPTOR_CONFIG, useValue: interceptorProviders },
      ...interceptorProviders
    ],
  };
}