Login/Logout, Proteção de rotas e envio de tokens com Angular
Sumário
- Introdução
- Primeiros passos
- Criando nossos componentes
- Tela de login
- Protegendo nossas rotas
- Enviando o token do usuário nas requisições
- Finalizando
Introdução
Fazer login/logout, muitas vezes, parece ser algo muito simples, porém, ao nos depararmos com a necessidade do mesmo, vemos que não é tão simples assim, ainda mais para quem não tem tanta intimidade com storage, route guards e token interceptors. Neste artigo, irei explicar de forma simples e prática, como realizar um login/logout completo e funcional utilizando Angular.
Primeiros passos
Os primeiros passos é entender o que utilizaremos e o porquê utilizaremos:
Storage: Será o responsável por armazenar os dados do usuário logado localmente, para que possamos utilizá-los quando necessário (IdUsuario, Token, Nome, etc). Se possível, utilize criptografia para armazenar esses dados, pois eles serão visíveis.
Route guards ou guarda de rotas: Será o responsável por verificar se o usuário que está acessando determinada rota, está logado ou não, e assim, redirecioná-lo caso seja necessário.
Token interceptor: Será o responsável por interceptar nossas chamadas https que serão realizadas para alguma API, e que a mesma necessite do token do usuário. Exemplo: o endereço base da nossa API é https://localhost:5001/v1. Sempre que houver uma chamada para essa API, nosso token interceptor irá interceptar essa chamada e adicionar no header o token do usuário. Com isso, você não precisará colocar o token em todas as chamadas que irá fazer para essa API.
Bora lá!
Irei partir do princípio que você já saiba criar um projeto em Angular e tenha os conhecimentos prévios no framework. E caso não saiba, a documentação é muito rica, basta consultar.
Com o projeto já criado, irei utilizar a seguinte estrutura:
Estilizando o projeto
Para estilizar minhas páginas, formulários, botões, etc, irei utilizar o Angular Material, porém, você pode utilizar o framework de sua preferência.
Caso queira utilizar o mesmo que eu, basta digitar no seu terminal: ng add @angular/material
Em seguida escolher um tema e dar y (YES) para sempre rsrs.
Iremos importar os módulos que vamos utilizar do Material em nosso módulo principal app.module.ts, e aproveitando a viagem, iremos importar os módulos necessários para nossos formulários reativos e nossas chamadas HTTPs, que ficará assim:
Criando nossos componentes
Agora iremos criar três páginas/componentes, que serão: a Home (que o usuário só poderá acessar caso já esteja logado), a Login (que o usuário só poderá acessar caso não esteja logado) e a Principal (que geralmente utilizamos como layout base, com header e footer).
Utilizaremos os seguintes comandos para criar nossos components diretamente na nossa pasta pages:
ng g c pages/login
ng g c pages/home
ng g c pages/compartilhado/principal
Os arquivos .spec.ts poderão ser apagados, pois servem para testar a aplicação, e não iremos usá-los no momento.
Com esses três componentes criados, iremos adicionar suas respectivas rotas no arquivo app-routing-module.ts, que ficará assim, por enquanto:
Perceba que utilizamos uma jogadinha nas rotas, onde a Home e as demais que forem surgir e que também necessitem do layout padrão (header, menu) e login do usuário, serão rotas filhas do component principal.
Porém, para que tudo funcione como desejamos, será necessário realizar as seguintes alterações:
Primeiramente, apague tudo no arquivo app.component.html e deixe apenas o router-outlet, que como bem sabemos, é o responsável por acessar nossas rotas:
Agora precisamos inserir nosso HTML e CSS que será utilizado como layout padrão. Abaixo irei mostrar como ficou o meu, porém, você pode brincar à vontade com o seu, caso não queira fazer igual:
principal.component.html
<p>
<mat-toolbar color="primary">
<span>Exemplo login</span>
<span class="espacamento-toolbar"></span>
<button mat-icon-button>
<mat-icon>logout</mat-icon>
</button>
</mat-toolbar>
</p>
<mat-card-content>
<router-outlet></router-outlet>
</mat-card-content>
principal.component.scss
.espacamento-toolbar {
flex: 1 1 auto;
}
Observe que existe outro router-outlet no componente principal, e será justamente nesse ponto que as rotas filhas do componente principal serão carregadas. Para visualizarmos melhor, agora já podemos testar como está ficando nossa aplicação, utilize o comando ng serve --o
E olha só que bacana o que já temos na nossa rota raíz, o componente principal e seu filho home...
Tela de login
Agora, iremos para nossa página de login. Assim como na tela acima, irei passar rapidamente pelos arquivos e ressaltando apenas os pontos relevantes para este artigo. Sinta-se livre para desenhar a tela como desejar. Mas antes que eu me esqueça, iremos primeiro criar nosso usuario.service.ts que será onde teremos nossos métodos relacionados ao usuário.
Para criar seu service, utilize o comando ng g service services/usuario
. E já aproveitando a viagem, iremos criar nossa interface de Usuário, para tiparmos nossos dados. Na pasta interfaces, crie o arquive IUsuario.ts, que ficará assim:
export interface IUsuario{
id: string,
email: string;
senha: string
}
No arquivo usuario.service.ts, já irei criar todos os métodos que irei precisar para logar e deslogar nosso usuário:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
import { IUsuario } from '../interfaces/IUsuario';
const apiUrlUsuario = environment.apiUrl + "Usuario";
@Injectable({
providedIn: 'root'
})
export class UsuarioService {
constructor(private httpClient: HttpClient,
private router: Router) { }
logar(usuario: IUsuario) : Observable<any> {
/*return this.httpClient.post<any>(apiUrlUsuario + "/login", usuario).pipe(
tap((resposta) => {
if(!resposta.sucesso) return;
localStorage.setItem('token', btoa(JSON.stringify(resposta['token'])));
localStorage.setItem('usuario', btoa(JSON.stringify(resposta['usuario'])));
this.router.navigate(['']);
}));*/
return this.mockUsuarioLogin(usuario).pipe(tap((resposta) => {
if(!resposta.sucesso) return;
localStorage.setItem('token', btoa(JSON.stringify("TokenQueSeriaGeradoPelaAPI")));
localStorage.setItem('usuario', btoa(JSON.stringify(usuario)));
this.router.navigate(['']);
}));
}
private mockUsuarioLogin(usuario: IUsuario): Observable<any> {
var retornoMock: any = [];
if(usuario.email === "hello@balta.io" && usuario.senha == "123"){
retornoMock.sucesso = true;
retornoMock.usuario = usuario;
retornoMock.token = "TokenQueSeriaGeradoPelaAPI";
return of(retornoMock);
}
retornoMock.sucesso = false;
retornoMock.usuario = usuario;
return of(retornoMock);
}
deslogar() {
localStorage.clear();
this.router.navigate(['login']);
}
get obterUsuarioLogado(): IUsuario {
return localStorage.getItem('usuario')
? JSON.parse(atob(localStorage.getItem('usuario')))
: null;
}
get obterIdUsuarioLogado(): string {
return localStorage.getItem('usuario')
? (JSON.parse(atob(localStorage.getItem('usuario'))) as IUsuario).id
: null;
}
get obterTokenUsuario(): string {
return localStorage.getItem('token')
? JSON.parse(atob(localStorage.getItem('token')))
: null;
}
get logado(): boolean {
return localStorage.getItem('token') ? true : false;
}
}
Repare no nosso método logar(). Como não temos uma API para autenticar nosso usuário, irei mockar o retorno do método logar, para conseguirmos simular.
logar(usuario: IUsuario) : Observable<any> {
/*return this.httpClient.post<any>(apiUrlUsuario + "/login", usuario).pipe(
tap((resposta) => {
if(!resposta.sucesso) return;
localStorage.setItem('token', btoa(JSON.stringify(resposta['token'])));
localStorage.setItem('usuario', btoa(JSON.stringify(resposta['usuario'])));
this.router.navigate(['']);
}));*/
return this.mockUsuarioLogin(usuario).pipe(tap((resposta) => {
if(!resposta.sucesso) return;
localStorage.setItem('token', btoa(JSON.stringify("TokenQueSeriaGeradoPelaAPI")));
localStorage.setItem('usuario', btoa(JSON.stringify(usuario)));
this.router.navigate(['']);
}));
}
private mockUsuarioLogin(usuario: IUsuario): Observable<any> {
var retornoMock: any = [];
if(usuario.email === "hello@balta.io" && usuario.senha == "123"){
retornoMock.sucesso = true;
retornoMock.usuario = usuario;
retornoMock.token = "TokenQueSeriaGeradoPelaAPI";
return of(retornoMock);
}
retornoMock.sucesso = false;
retornoMock.usuario = usuario;
return of(retornoMock);
}
O que está acontecendo aqui é, por parâmetro irá chegar nosso usuário (email e senha) que chegarão do formulário de login (que iremos fazê-lo logo em seguida), no caso do nosso mock, se usuário digitado for hello@balta.io e a senha 123 iremos autenticar esse usuário, salvar o token e suas informações de usuário no storage e redirecioná-lo para nossa página home. Para salvar os dados no storage, utilizei o btoa e atob, que apenas irão passar os dados para base 64, para não ficarem tão visíveis no navegador.
E ressaltando que, a forma que você irá tratar o retorno do seu método logar, irá depender muito de como a API está lhe retornando os dados, porém, em grande parte das vezes, são retornados dessa forma.
Em seguida, precisamos fazer nosso formulário de login e fazê-lo funcionar com nosso usuario.service.
Para validação do formulário, utilizei Reactive Forms, que ficou dessa maneira:
login.component.html
<mat-card-content class="container">
<mat-card class="card-login">
<mat-card-title>Login</mat-card-title>
<form [formGroup]="formLogin" novalidate>
<mat-card-content class="conteudo-login">
<mat-form-field appearance="outline" class="form-field-full">
<mat-label>Email</mat-label>
<input type="email" matInput formControlName="email" placeholder="Ex. perukas@example.com">
<mat-error *ngIf="formLogin.get('email').hasError('email') && formLogin.get('email').touched">
Por favor informe um email válido
</mat-error>
<mat-error *ngIf="formLogin.get('email').hasError('required') && formLogin.get('email').touched">
Email é <strong>obrigatório</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field-full">
<mat-label>Senha</mat-label>
<input type="password" matInput formControlName="senha">
<mat-error *ngIf="formLogin.get('senha').invalid && formLogin.get('senha').touched">Senha é
<strong>obrigatória</strong>
</mat-error>
</mat-form-field>
</mat-card-content>
</form>
<mat-card-actions align="end">
<button mat-button color="primary" class="full" align="end" (click)="logar()">Entrar</button>
</mat-card-actions>
</mat-card>
</mat-card-content>
login.component.scss
.container{
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
.conteudo-login{
display: flex;
flex-direction: column;
}
.card-login{
width: 300px;
box-shadow: 10px 5px 5px rgba(0, 0, 0, 0.4);
}
login.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { IUsuario } from '../../interfaces/IUsuario';
import { UsuarioService } from '../../services/usuario.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
formLogin: FormGroup;
constructor(private formBuilder: FormBuilder,
private usuarioService: UsuarioService,
private snackBar: MatSnackBar) { }
ngOnInit(): void {
this.criarForm();
}
criarForm(){
this.formLogin = this.formBuilder.group({
email: ['', [Validators.required, Validators.email]],
senha: ['', [Validators.required]]
});
}
logar(){
if(this.formLogin.invalid) return;
var usuario = this.formLogin.getRawValue() as IUsuario;
this.usuarioService.logar(usuario).subscribe((response) => {
if(!response.sucesso){
this.snackBar.open('Falha na autenticação', 'Usuário ou senha incorretos.', {
duration: 3000
});
}
})
}
}
E olha só que legal o que temos até agora:
Protegendo nossas rotas
Bom, realizada as etapas acima, agora precisamos proteger nossas rotas. Se não, qual seria o sentido de ter um login, se o usuário pode digitar na url "http://localhost:4200/" e acessar sem login, não é mesmo?!
Para proteger nossas rotas, iremos criar dois guardas de rotas. Um será para verificar se o usuário já está logado e direcioná-lo para nossa home, e o outro será para verificar se ele não está logado e direcioná-lo para nosso login.
Para criar os dois guardas de rotas, utilize os seguintes comandos:
Os dois serão CanActivate
ng g guard services/guards/usuario-nao-autenticado
ng g guard services/guards/usuario-autenticado
E os nossos arquivos ficarão assim:
usuario-autenticado.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { UsuarioService } from '../usuario.service';
@Injectable({
providedIn: 'root'
})
export class UsuarioAutenticadoGuard implements CanActivate{
constructor(
private usuarioService: UsuarioService,
private router: Router) { }
canActivate(){
if (this.usuarioService.logado) {
return true;
}
this.router.navigate(['login']);
return false;
}
}
usuario-nao-autenticado.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { UsuarioService } from '../usuario.service';
@Injectable({
providedIn: 'root'
})
export class UsuarioNaoAutenticadoGuard implements CanActivate{
constructor(
private usuarioService: UsuarioService,
private router: Router) { }
canActivate(){
if (this.usuarioService.logado) {
this.router.navigate(['']);
return false;
}
return true;
}
}
Agora, basta colocar nossos guardas de rotas no arquivo app-routing.module.ts
, que ficará assim as rotas:
const routes: Routes = [
{ path: 'login', component: LoginComponent, canActivate: [UsuarioNaoAutenticadoGuard]},
{
path: '', component: PrincipalComponent, canActivate: [UsuarioAutenticadoGuard],
children: [
{ path: '', component: HomeComponent }
],
},
];
Para conseguirmos realizar o teste completo, precisamos adicionar a função de logout no nosso menu principal. Irei adicionar o método deslogar no evento click do meu botão logout:
principal.component.html
<button mat-icon-button (click)="deslogar()">
<mat-icon>logout</mat-icon>
</button>
E no arquivo principal.component.ts
, basicamente, irei injetar meu usuarioService e chamar o método deslogar(), que irá limpar o storage e redirecionar o usuário para o login:
import { Component, OnInit } from '@angular/core';
import { UsuarioService } from '../../../services/usuario.service';
@Component({
selector: 'app-principal',
templateUrl: './principal.component.html',
styleUrls: ['./principal.component.scss']
})
export class PrincipalComponent implements OnInit {
constructor(private usuarioService: UsuarioService) { }
ngOnInit(): void {
}
deslogar(){
this.usuarioService.deslogar();
}
}
Para testar tudo isso, você pode tentar acessar a rota raíz sem estar logado e/ou tentar acessar a rota login estando logado.
Feito isso, temos um login totalmente funcional!!!!!!! \õ/
Enviando o token do usuário nas requisições
Como já sabemos, o Angular interage muito bem com comunicações via API, porém, sempre que nos comunicamos via API, precisamos enviar o token do usuário, que comprova que o usuário já está autenticado em determinada API e pode realizar as requisições de acordo com suas permissões. Para isso, iremos utilizar os interceptors. Utilize o comando ng g interceptor services/interceptors/token
para gerá-lo. Nosso token.interceptor.ts, ficará assim:
token.interceptor.ts
import { environment } from 'src/environments/environment';
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { UsuarioService } from '../usuario.service';
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs/internal/observable/throwError';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private usuarioService : UsuarioService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.usuarioService.obterTokenUsuario;
const requestUrl: Array<any> = request.url.split('/');
const apiUrl: Array<any> = environment.apiUrl.split('/');
if (token && requestUrl[2] === apiUrl[2]) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
token: `${token}`
}
});
return next.handle(request).pipe(catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401)
this.usuarioService.deslogar(false);
else
return throwError(error.message);
}));
}
else {
return next.handle(request);
}
}
}
A grosso modo, o que está acontecendo no código acima é:
Estamos obtendo o token do usuário que está no nosso storage;
Estamos obtendo a requisição que está sendo executada;
Estamos obtendo a apiUrl que está em nosso environment;
É verificado se a requisição que está sendo realizada, é de fato para a API que temos em nosso environment;
Caso for, adicionamos o token no header da requisição;
Caso não for, prosseguimos a requisição normal;
Ao terminar a requisição, verificamos se houve algum erro.
Caso ocorra o erro 401 Unauthorized no retorno da requisição, ou seja, nosso usuário não está mais autenticado na API, iremos deslogar o usuário. Afinal, sem o token não conseguiremos mais fazer requisições para as rotas que necessitam de autenticação.
Para fechar nosso token-interceptor por completo, precisamos adicioná-lo nos providers no app.module.ts
, que ficará assim:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './pages/login/login.component';
import { HomeComponent } from './pages/home/home.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { PrincipalComponent } from './pages/compartilhado/principal/principal.component';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TokenInterceptor } from './services/interceptors/token.interceptor';
@NgModule({
declarations: [
AppComponent,
LoginComponent,
HomeComponent,
PrincipalComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
HttpClientModule,
ReactiveFormsModule,
FormsModule,
MatCardModule,
MatInputModule,
MatButtonModule,
MatSidenavModule,
MatToolbarModule,
MatIconModule,
MatSnackBarModule
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
Finalizando
Bom, por ora é isso pessoal, temos um login totalmente funcional de ponta a ponta!
Espero que com esse artigo, eu tenha contribuído com suas maiores dúvidas em relação ao tema e até a próxima. Muito obrigado pela leitura até aqui!
Código disponibilizado no GitHub → https://github.com/GustavoRodrigues94/AngularLoginExemplo