Solucionado (ver solução)
Solucionado
(ver solução)
10
respostas

Adicionando Token em toda requisão com Angular 2 [Solução]

Boa noite pessoal, Boa noite Flavio,

Fiz o curso de MEAN usando Angular 2 que por sua vez não possui o interceptors. Então me virei aqui e consegui fazer funcionar. Para ajudar vou divulgar as alterações que fiz aqui para quem mais quiser saber como.

Primeira alteração que fiz foi sobreescrever o XHRBackend, ele é um componente resposável por criar a conexão. E nele que adiciono os Headers necessários

import { Injectable } from '@angular/core';
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';

@Injectable()
export class ExtendedXHRBackend extends XHRBackend {

  constructor(browserXhr: BrowserXhr, baseResponseOptions: ResponseOptions, xsrfStrategy: XSRFStrategy) {
    super(browserXhr, baseResponseOptions, xsrfStrategy);
  }

  createConnection(request: Request) {
    let token = localStorage.getItem('token');
    request.headers.set('x-access-token', `${token}`); 
    request.headers.set('Content-Type', 'application/json');
    let xhrConnection = super.createConnection(request);
    xhrConnection.response = xhrConnection.response.catch((error: Response) => {
      if (error.status === 401 || error.status === 403) {
        console.log('acesso não autorizado');
        localStorage.removeItem('token');
      }
      return Observable.throw(error);
    });
    return xhrConnection;
  }
}

Para que ele injete essa classe precisamos adicionar no AppModule

providers: [{ provide: XHRBackend, useClass: ExtendedXHRBackend }]

Ótimo isso já funcionaria para adicionar no request! Mas como redirecionar para o Login?

O Angular 2 possui em suas rotas o atributo canActivate que recebe um Array de classes que implementam CanActivate.

Então criamos o nosso Guarda

import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { UserService } from './user.service';

@Injectable()
export class LoggedInGuard implements CanActivate {

  constructor(private user: UserService, private router: Router) {}

  canActivate() {
      let isLoggedIn = this.user.isLoggedIn();
      if(!isLoggedIn){
          this.router.navigate(['']);
      }
      return isLoggedIn;
  }
}

E usamos em nossas rotas (fiz algumas alterações)

const appRoutes: Routes = [    
    { path: '', component: LoginComponent },
    { path: 'cadastro', component: CadastroComponent, canActivate: [LoggedInGuard] },
    { path: 'cadastro/:id', component: CadastroComponent, canActivate: [LoggedInGuard] },
    { path: 'home', component: ListagemComponent, canActivate: [LoggedInGuard] },
    { path: '**', component: LoginComponent, canActivate: [LoggedInGuard] }
];

A classe UserService é a responsável por autenticar e informar se o usuário esta logado ou não

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Usuario } from '../login/login.component';
import { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Injectable()
export class UserService {

  private url: string = 'v1/autentica';

  private _loggedIn: BehaviorSubject<boolean> = new BehaviorSubject(false);
  public loggedIn: Observable<boolean> = this._loggedIn.asObservable();

  constructor(private http: Http) { }

  autentica(usuario: Usuario) {
      return this.http
                 .post(this.url, JSON.stringify(usuario))
                 .map((res) => {                     
                    var token = res.headers.get('x-access-token');
                    if (token) {
                        this._loggedIn.next(true);
                        localStorage.setItem('token', token);
                    }
                 });
  }

  logout() {
    localStorage.removeItem('token');
  }

  isLoggedIn() {
    let token = localStorage.getItem('token');

    if(token){ //essa atribuição é feita para atualizar a variavel e o resto do sistema ser notificado
      this._loggedIn.next(true);
    } else {
      this._loggedIn.next(false);
    }

    return this._loggedIn.getValue();
  }

}

Observe que usei o objeto BehaviorSubject, não entrarei em detalhes aqui, mas ele é e gera um observable que permite que outras classes possam saber das mudanças. Mas para que fazer isso? Nesse caso foi um capricho para permitir que a navbar fosse exibida apenas se o usuário estivesse logado. Para isso é só adicionar o UserService no AppComponent

constructor(private user: UserService){ }

e utiliza-lo no html

<nav class="navbar navbar-default" *ngIf="user.loggedIn | async" >
    <ul class="nav navbar-nav">
      <li><a [routerLink]="['/cadastro']">Nova</a></li>
      <li><a [routerLink]="['/home']">Home</a></li>
    </ul>
</nav>
<router-outlet></router-outlet>

o pipe async fara o subcripte na váriavel user.loggedIn, portanto toda vez que a váriavel mudar será refletida na navbar

Obs Não esqueçam de adicionar o LoggedInGuard e UserServide no AppModule. O resultado final fica assim.

    providers: [{ provide: XHRBackend, useClass: ExtendedXHRBackend }, 
                LoggedInGuard, 
                UserService],

Fica aí minha contribuição com o curso. Espero que seja útil.

Abraços

10 respostas
solução!

Como referências utilizei 3 tutoriais

1 - http://www.adonespitogo.com/articles/angular-2-extending-http-provider/

2 - https://medium.com/@blacksonic86/angular-2-authentication-revisited-611bf7373bf9#.5019djlj9

3 - http://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/

O primeiro diz para sobreescrever o HTTP mas para mim não funcionou, e não sei porque. Em um dos comentários esta a solução que utilizei.

O segundo ensina sobre o LoggedInGuard, preferi deixar ele responsável pelo redirect e o ExtendedXHRBackend apenas para adicionar ao header

O terceiro me ensinou o que precisava saber sobre Observable, Subject e BehaviorSubject. Ele não coloca os imports, então adicionei nos exemplos aqui

Obrigado Vinicius!!! Vou marcar sua segunda resposta como solução e espero que outros alunos tirem proveito desse conhecimento :).

Ótimo tutorial, funcionou no meu projeto. Mas tenho uma dúvida ainda:

Quando eu uso o Angular 2 para consumir a API do Node externamente, ele não consegue ter acesso aos headers do response, logo, não consegue obter o token.

Se eu colocar meu projeto Angular 2 dentro do node, no diretorio /public, tudo funciona perfeitamente.

Imagino que seja um problema de CORS. Tenho isso na minha configuração do Express:

app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "http://localhost:4200");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, X-XSRF-TOKEN, x-access-token, Authorization, Content-Type, Accept");
    next();
});

Mas mesmo assim não consigo acessar o header do response no tentar autenticar.

Alguma ideia?

Gostaria de adicionar uma correção nessa minha resposta que ficou bem melhor.

A alteração é sobreescrever o HTTP ao invés do XHRBackend.

A solução utilizada é da primeira referência do Adones http://www.adonespitogo.com/articles/angular-2-extending-http-provider/

import {Injectable} from '@angular/core';
import {Http, XHRBackend, RequestOptions, Request, RequestOptionsArgs, Response, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class HttpService extends Http {

  constructor (backend: XHRBackend, options: RequestOptions) {
    let token = localStorage.getItem('auth_token'); // your custom token getter function here
    options.headers.set('Authorization', `Bearer ${token}`);
    super(backend, options);
  }

  request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
    let token = localStorage.getItem('auth_token');
    if (typeof url === 'string') { // meaning we have to add the token to the options, not in url
      if (!options) {
        // let's make option object
        options = {headers: new Headers()};
      }
      options.headers.set('Authorization', `Bearer ${token}`);
    } else {
    // we have to add the token to the url object
      url.headers.set('Authorization', `Bearer ${token}`);
    }
    return super.request(url, options).catch(this.catchAuthError(this));
  }

  private catchAuthError (self: HttpService) {
    // we have to pass HttpService's own instance here as `self`
    return (res: Response) => {
      console.log(res);
      if (res.status === 401 || res.status === 403) {
        // if not authenticated
        console.log(res);
      }
      return Observable.throw(res);
    };
  }
}

e para que funcione automatico para todas as suas requisições é só adicionar no providers

{ provide: Http, useClass: ExtendedHttpService },

Caso você tenha feito o do XHRBackend, remova.

Flavio, se achar melhor crio um post novo com essa solução final

Yuri,

No meu expresse tenho os seguintes códigos

Para o consign

consign({ cwd: 'app'})
         .include('models')
         .then('api')
         .then('routes/auth.js')
         .then('routes')
         .into(app);

Para retornar sempre o index.hml

app.all('/*', function(req, res, next) {
    res.sendFile(path.join(app.get('clientPath'), 'index.html'));
});

routes/auth.js

module.exports = function(app) {
    var api = app.api.auth;
    app.post('/v1/autentica/', api.autentica);
    app.use('/v1/*', api.verificaToken);
};

A ideia aqui é que caso acesse a url direto, redirecione para o html e caso acesse /v1/ irá consultar a api.

Espero que ajude e desculpa a demora, não estava tendo tempo para olhar com calma

Olá Vinicius,

Já reimplementei conforme seu exemplo do HttpService.

Mas um problema ainda persiste e não sei o que pode ser.

Quando tento realizar o login, ele envia uma requisção OPTIONS antes da POST. Essa options vai na rota verficaToken. Como ele ainda não fez a autenticação do POST, recebo um 401.

Sabe porque ele envia esse OPTIONS primeiro?

Olá,

Resolvi os problemas da seguinte forma. Não sei se é a melhor forma, mas está tudo funcionando agora.

Sobre o OPTIONS, li que tem a ver com o fato do meu Client Angular estar separado da API, em outro endereço. Logo, esse é um verbo para o CORS.

Esse caso resolvi devolvendo um 200 para o options, antes que chegue nas rotas.

app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Expose-Headers", "x-access-token");
    res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
    res.header("Access-Control-Allow-Headers", "x-access-token, Origin, X-Requested-With, X-XSRF-TOKEN, Authorization, Content-Type, Accept");

    if ('OPTIONS' === req.method) {
      res.sendStatus(200);
    }
    else {
      next();
    }
});

Outro problema que eu tinha é com as requisições enviando o token. Vi que na sua nova implementação você usa o seguinte trecho para incluir o token:

options.headers.set('Authorization', `Bearer ${token}`);

Porém meu backend NodeJS esperava esse header:

options.headers.set('x-access-token', `${token}`);

Está tudo funcionando agora. Mas a pergunta que fica é: O que fiz é a melhor coisa a se fazer?

Boa noite,

Estou com o mesmo problema do @Yuri Bett, primeiro minha aplicação envia um request method: OPTIONS, retorna o status 200 depois que envia o method: POST que vem com o Authorization.

Como que faço para corrigir? Se alguém puder compartilhar agradeço.

Att.

Alguma solução, gente? Estou fazendo esse curso também ...

Oi eu não conseguiu solucionar este problema e fui para uma forma alternativa para fazer o mesmo ... se conseguir resolver agradeço se puder compartilhar.