Solucionado (ver solução)

Importante

Você está vendo a versão anterior da nova experiência da Alura que estamos preparando para você. Em breve, ela ganha uma identidade visual novinha totalmente pensada em potencializar seus estudos!

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.