Interceptor - Angular

Considere um projeto onde o usuário efetua o login, recebe um JWT como resultado da autenticação e então o usuário pode consultar ou editar suas informações de perfil passando o JWT para se identificar. Os principais arquivos do projeto seriam mais ou menos o seguinte:

projeto/
├── components/
│   ├── login.component.ts
│   └── profile.component.ts
├── services/
│   ├── login.service.js
│   └── profile.service.ts

Repare bem que após o usuário efetuar o login, ele recebe um JWT que podemos guardar na sessionStorage por exemplo.

Para obter as informações do perfil do usuário logado ou editá-las, é necessário informar na requisição o JWT para que a API saiba quem é o usuário. Então o arquivo profile.service.ts teria a seguinte cara:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class ProfileService {
  constructor(private http: HttpClient) { }
  
  obterPerfil() {
     const httpOptions = {
      headers: new HttpHeaders({
        Authorization: 'JWT obtido no login'
      })
    };

    this.http.get('algum endereço', httpOptions);
  }
  
  editarPerfil(dados: any) {
    const httpOptions = {
      headers: new HttpHeaders({
        Authorization: 'JWT obtido no login'
      })
    };

    this.http.put('algum endereço', httpOptions);
  }
}
profile.service.ts

Repare que informamos o header Authorization nas duas requisições.

Até este momento estamos falando de uma única service, ProfileService, mas imagine que este sistema irá crescer e teremos diversas outras services. Vamos ter que adicionar em todas as requisições o header Authorization, mas a forma como foi demonstrado acima não é a melhor saída. O ideal é que alguém possa interceptar todas as requisições e adicionar este header Authorization de forma automática. Este alguém é o HttpInterceptor.

Sempre que for necessário interceptar as requisições partindo da sua aplicação, o interceptor é o recurso que te ajudará.

HttpInterceptor

Para construir o seu próprio interceptor é necessário herdar a interface HttpInterceptor que está em @angular/common/http. A interface tem o seguinte formato:

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

Logando as requisições com HttpInterceptor

Como primeiro exemplo vou mostrar como implementar e utilizar um interceptor com o objetivo de "printar" no console algumas informações sobre as requisições que sua aplicação Angular está fazendo:

import {
    HttpEvent,
    HttpHandler,
    HttpInterceptor,
    HttpRequest,
} from '@angular/common/http';
import { Observable } from 'rxjs';

export class LogInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): 
      Observable<HttpEvent<any>> {

        // Observação:
        // const { method, url } = req;
        //
        // é a mesma coisa que
        //
        // const method = req.method;
        // const url    = req.url;

        const { method, url } = req;
        console.log(`INTERCEPTOR ${method} ${url}`);

        return next.handle(req);
    }
}
log.interceptor.ts

A implementação da classe é bem simples, basta implementar a interface HttpInterceptor e "rechear" o método intercept.

Falando um pouco deste método, intercept, ele recebe dois parâmetros:

  1. req: HttpRequest<any>: objeto com as informações da requisição que foi efetuada na sua aplicação.
  2. next: HttpHandler: representa o próximo interceptor da sua cadeia, ou seja, sua aplicação pode ter vários interceptors, então o next indica que ou passamos para o próximo interceptor ou a requisição já pode ser efetuada para o backend.

Bom, já com a classe implementada, precisamos "dizer" ao Angular que é para usar este Interceptor. Para isto, podemos indicar através de um provider no AppModule:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

import { AppComponent } from './app.component';
import { HelloComponent } from './hello.component';
import { LogInterceptor } from './interceptor/log.interceptor';

@NgModule({
  imports:      [ BrowserModule, FormsModule, HttpClientModule ],
  declarations: [ AppComponent, HelloComponent ],
  bootstrap:    [ AppComponent ],
  // ***************************************************************
  // Registrando o interceptor
  providers:    [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LogInterceptor,
      multi: true   
    }
  ]
  // ***************************************************************
})
export class AppModule { }
app.module.ts

Observação: a service utilizada neste exemplo não tem conteúdo relevante. Ela possui 2 métodos que efeturam requisições distintas. De qualquer forma no final do artigo deixei um link com o projeto completo.

Resultado:

Nosso primeiro interceptor em ação

Veja no GIF acima que ao clicar nos botões Ativar teste 1 e Ativar teste 2, é efetuada uma requisição HTTP e nosso interceptor entra em ação efetuando os prints no console. Na service que faz a requisição não existe nenhuma referencia a estes console.log ou ao nosso interceptor:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class MockService {

  constructor(private http: HttpClient) { }

  teste1() {
    const url = 'https://run.mocky.io/v3/dfd8147a-17ca-4b9c-8887-831c7d99e104';
    return this.http
      .get<Array<{ name: string, city: string }>>(url);
  }

  teste2() {
    const url = 'https://run.mocky.io/v3/b4b86d79-014b-49c4-9703-e3f1442a6507';
    return this.http
      .get<{ name: string, city: string }>(url);
  }
}
mock.service.ts

Repare bem que não importa a quantidade de services que você terá em seu projeto, todas as requisições serão interceptadas pelo LogInterceptor no caso do nosso exemplo.

Modificando as requisições com HttpInterceptor

Agora que vimos como construir e utilizar um HttpInterceptor, vamos avançar um pouco mais e modificar todas as requisições com o objetivo de adicionar o cabeçalho (header) Authorization com o JWT comentado no início deste artigo.

Então vamos criar um outro interceptor chamado AuthorizationInterceptor:

import {
    HttpHandler,
    HttpInterceptor,
    HttpRequest,
} from '@angular/common/http';

export class AuthorizationInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    console.log('AuthorizationInterceptor');
    
    // Observação: caso não tenha nada no sessionStorage, que é
    // o caso deste exemplo, o valor 'teste' será o default:
    const jwt = sessionStorage.getItem('jwt') || 'teste';

    // Clona a requisição porque não podemos alterar
    // a instância do HttpHeaders, ela é imutável
    const authReq = req.clone({
      headers: req.headers.set('Authorization', jwt)
    });

    // Devolvemos a nova requisição com o header Authorization:
    return next.handle(authReq);
  }
}
authorization.interceptor.ts

Como dito anteriormente, vamos registrar este novo interceptor no AppModule:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

import { AppComponent } from './app.component';
import { HelloComponent } from './hello.component';
import { LogInterceptor } from './interceptor/log.interceptor';
import { AuthorizationInterceptor } from './interceptor/authorization.interceptor';

@NgModule({
  imports:      [ BrowserModule, FormsModule, HttpClientModule ],
  declarations: [ AppComponent, HelloComponent ],
  bootstrap:    [ AppComponent ],
  providers:    [{
                    provide: HTTP_INTERCEPTORS,
                    useClass:
                    LogInterceptor,
                    multi: true
                 }, {
                    provide: HTTP_INTERCEPTORS,
                    useClass: AuthorizationInterceptor,
                    multi: true
                 }]
})
export class AppModule { } 

Observe que a partir de agora temos 2 interceptors e o header Authorization passa a existir em todas as requisições efetuadas pela aplicação.

As setas na imagem abaixo indicam os prints que os 2 interceptores efetuam e o quadrado vermelho a direita indica a presença do novo header Authorization:

O interceptor adiciona o header Authorization para todas as requisições

Pensando em nosso cenário inicial, onde precisaríamos passar o header Authorization em todas as requisições, a melhor forma de fazer isto é com um interceptor conforme o exemplo acima.

Considerações

Perceba que o interceptor pode ajudar em vários casos, podemos fazer um log das requisições, modificar cabeçalhos, modificar o próprio corpo da requisição, tratar erros de uma forma mais genérica, etc. Deixo como sugestão a leitura deste link.

Exemplo completo:

angular-como-utilizar-httpinterceptor - StackBlitz
Exemplo de utilização do interceptor

Links: