Login/Logout, Proteção de rotas e envio de tokens com Angular
Criar um login com Angular parece simples à primeira vista, mas o fluxo completo envolve storage, guardas de rotas e interceptors trabalhando juntos. Neste artigo, você vai montar de forma prática um login e um logout funcionais, protegendo rotas e enviando o token do usuário para a API automaticamente.
O que você precisa para um login com Angular
Antes de escrever código, vale entender as três peças que sustentam o fluxo. Cada uma resolve um problema diferente, e juntas formam a base de qualquer login com Angular.
Além disso, entender o papel de cada peça ajuda a depurar o login com Angular quando algo não se comporta como esperado.
Storage
O storage guarda localmente os dados do usuário autenticado, como o identificador, o token e o nome. Dessa forma, a aplicação consulta essas informações sempre que precisar, sem pedir um novo login a cada navegação.
Esses dados ficam visíveis no navegador, por isso a recomendação do exemplo original é usar alguma forma de criptografia quando possível. Mais adiante, você vai ver por que a simples conversão para Base64 não protege nada no login com Angular.
Route guards
Os route guards, ou guardas de rotas, verificam se quem acessa uma rota está autenticado ou não. Assim, o próprio roteador redireciona o usuário quando ele tenta entrar em uma página que não deveria ver.
Se você quiser se aprofundar no roteamento do framework, o artigo sobre rotas, guardas e navegação no Angular complementa bem este conteúdo.
Token interceptor
O token interceptor intercepta as chamadas HTTP feitas para a API que exige autenticação. Por exemplo, imagine que o endereço base da API é https://localhost:5001/v1.
Sempre que houver uma chamada para esse endereço, o interceptor adiciona o token do usuário no cabeçalho. Com isso, você não precisa repetir o token manualmente em cada requisição do projeto.
Preparando o projeto de login com Angular
Este tutorial de login com Angular parte do princípio de que você já sabe criar um projeto e conhece o básico do framework. Caso ainda não saiba, a documentação oficial é bastante completa e vale a consulta.
Com o projeto criado, a estrutura de pastas usada no exemplo fica assim, com páginas, serviços, guards, interceptors e interfaces separados:
Estilizando com Angular Material
Para estilizar páginas, formulários e botões, o exemplo usa o Angular Material. No entanto, você pode escolher o framework visual de sua preferência sem nenhum impacto na lógica do login com Angular.
Para usar a mesma biblioteca, execute o comando abaixo no terminal. Em seguida, escolha um tema e confirme as opções sugeridas pelo instalador.
ng add @angular/material
Depois disso, importe no módulo principal app.module.ts os módulos do Material que a tela vai usar. Aproveite também para importar os módulos de formulários reativos e de chamadas HTTP, que o login vai precisar.
Criando os componentes do login com Angular
Agora você vai criar três páginas. A primeira é a Home, que o usuário só acessa quando está autenticado.
A segunda é a Login, que o usuário só acessa quando ainda não entrou no sistema. Por fim, a terceira é a Main, que funciona como layout base, com cabeçalho e área de conteúdo.
Use os comandos abaixo para gerar os componentes diretamente na pasta pages:
ng g c pages/login
ng g c pages/home
ng g c pages/shared/main
Você pode apagar os arquivos .spec.ts neste momento, porque servem para testes e não fazem parte do escopo do tutorial.
Configurando as rotas
Com os três componentes criados, adicione as rotas correspondentes no arquivo app-routing.module.ts. A primeira versão fica como a imagem abaixo, ainda sem nenhuma proteção.
Repare no truque usado nas rotas do login com Angular. A Home e todas as próximas páginas que precisarem do layout padrão e de autenticação viram rotas filhas do componente de layout.
Para que tudo funcione, apague o conteúdo do arquivo app.component.html e mantenha apenas o router-outlet. Afinal, ele é o responsável por renderizar o componente da rota ativa.
Montando o layout principal
Em seguida, escreva o HTML e o CSS do layout padrão. O exemplo abaixo usa uma toolbar com o botão de logout, mas você pode desenhar o seu como preferir.
main.component.html
<p>
<mat-toolbar color="primary">
<span>Login example</span>
<span class="toolbar-spacer"></span>
<button mat-icon-button>
<mat-icon>logout</mat-icon>
</button>
</mat-toolbar>
</p>
<mat-card-content>
<router-outlet></router-outlet>
</mat-card-content>
main.component.scss
.toolbar-spacer {
flex: 1 1 auto;
}
Observe que existe outro router-outlet dentro do layout. É justamente nesse ponto que o Angular carrega as rotas filhas do componente principal.
Para visualizar o resultado, rode ng serve --open e abra a rota raiz no navegador.
Nesse ponto, o layout do login com Angular já aparece, embora qualquer pessoa ainda consiga acessar a Home sem se autenticar.
Criando o serviço de usuário
Antes da tela, crie o serviço que concentra os métodos relacionados ao usuário. Ele é o coração do login com Angular, porque todos os outros componentes dependem dele.
Para isso, use o comando ng g service services/user.
Aproveite também para criar a interface que tipa os dados do usuário. Na pasta interfaces, crie o arquivo IUser.ts:
export interface IUser {
id: string;
email: string;
password: string;
}
No arquivo user.service.ts, já ficam todos os métodos necessários para entrar e sair da aplicação:
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 { IUser } from '../interfaces/IUser';
const userApiUrl = environment.apiUrl + 'User';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(private httpClient: HttpClient,
private router: Router) { }
login(user: IUser): Observable<any> {
// Versão real, chamando a API:
// return this.httpClient.post<any>(userApiUrl + '/login', user).pipe(
// tap((response) => {
// if (!response.success) return;
// localStorage.setItem('token', btoa(JSON.stringify(response['token'])));
// localStorage.setItem('user', btoa(JSON.stringify(response['user'])));
// this.router.navigate(['']);
// }));
// Versão simulada (mock), sem API
return this.mockUserLogin(user).pipe(tap((response) => {
if (!response.success) return;
localStorage.setItem('token', btoa(JSON.stringify('TokenGeneratedByApi')));
localStorage.setItem('user', btoa(JSON.stringify(user)));
this.router.navigate(['']);
}));
}
private mockUserLogin(user: IUser): Observable<any> {
const mockResponse: any = {};
if (user.email === 'hello@balta.io' && user.password === '123') {
mockResponse.success = true;
mockResponse.user = user;
mockResponse.token = 'TokenGeneratedByApi';
return of(mockResponse);
}
mockResponse.success = false;
mockResponse.user = user;
return of(mockResponse);
}
logout() {
localStorage.clear();
this.router.navigate(['login']);
}
get loggedUser(): IUser {
return localStorage.getItem('user')
? JSON.parse(atob(localStorage.getItem('user')))
: null;
}
get loggedUserId(): string {
return localStorage.getItem('user')
? (JSON.parse(atob(localStorage.getItem('user'))) as IUser).id
: null;
}
get userToken(): string {
return localStorage.getItem('token')
? JSON.parse(atob(localStorage.getItem('token')))
: null;
}
get isLoggedIn(): boolean {
return localStorage.getItem('token') ? true : false;
}
}
Entendendo o mock do login com Angular
Repare no método login(). Como não existe uma API real neste exemplo, o serviço simula o retorno com o método mockUserLogin().
O método recebe o usuário, com e-mail e senha vindos do formulário. Então, se o e-mail for hello@balta.io e a senha for 123, o mock considera o usuário autenticado.
Nesse caso, o serviço salva o token e os dados do usuário no storage e redireciona para a Home. Caso contrário, ele apenas devolve success como falso.
Esse mock permite testar todo o login com Angular antes mesmo de existir um back end pronto.
A versão comentada mostra como ficaria a chamada real com HttpClient. Na prática, o tratamento do retorno depende de como a sua API devolve os dados, embora esse formato com token e usuário seja bastante comum.
Base64 não é criptografia
Para gravar os dados, o exemplo usa btoa e atob, que apenas convertem o conteúdo para Base64. O objetivo é só evitar que os valores fiquem legíveis à primeira vista no navegador.
Contudo, qualquer pessoa decodifica Base64 em segundos, porque se trata de uma codificação e não de uma cifra. Portanto, não trate essa conversão como camada de segurança.
Além disso, qualquer script que rode na página consegue ler o localStorage. Por esse motivo, um ataque de XSS pode roubar um token guardado ali, como explica este comparativo entre localStorage e cookies.
Em cenários mais sensíveis, avalie cookies httpOnly e secure, que o JavaScript não consegue ler. Ainda assim, essa opção exige cuidados contra CSRF e mudanças na API.
Construindo a tela de login com Angular
Com o serviço pronto, chegou a hora do formulário que o usuário vai preencher no login com Angular. Para a validação, o exemplo usa Reactive Forms, que deixam as regras no TypeScript e o template mais limpo.
Template em login.component.html:
<mat-card-content class="container">
<mat-card class="login-card">
<mat-card-title>Login</mat-card-title>
<form [formGroup]="loginForm" novalidate>
<mat-card-content class="login-content">
<mat-form-field appearance="outline" class="form-field-full">
<mat-label>Email</mat-label>
<input type="email" matInput formControlName="email" placeholder="e.g. john@example.com">
<mat-error *ngIf="loginForm.get('email').hasError('email') && loginForm.get('email').touched">
Please enter a valid email
</mat-error>
<mat-error *ngIf="loginForm.get('email').hasError('required') && loginForm.get('email').touched">
Email is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field-full">
<mat-label>Password</mat-label>
<input type="password" matInput formControlName="password">
<mat-error *ngIf="loginForm.get('password').invalid && loginForm.get('password').touched">
Password is <strong>required</strong>
</mat-error>
</mat-form-field>
</mat-card-content>
</form>
<mat-card-actions align="end">
<button mat-button color="primary" (click)="login()">Sign in</button>
</mat-card-actions>
</mat-card>
</mat-card-content>
Estilos em login.component.scss:
.container {
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
.login-content {
display: flex;
flex-direction: column;
}
.login-card {
width: 300px;
box-shadow: 10px 5px 5px rgba(0, 0, 0, 0.4);
}
Lógica em login.component.ts:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { IUser } from '../../interfaces/IUser';
import { UserService } from '../../services/user.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
loginForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private userService: UserService,
private snackBar: MatSnackBar) { }
ngOnInit(): void {
this.createForm();
}
createForm() {
this.loginForm = this.formBuilder.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required]]
});
}
login() {
// Não envia nada se o formulário estiver inválido
if (this.loginForm.invalid) return;
const user = this.loginForm.getRawValue() as IUser;
this.userService.login(user).subscribe((response) => {
if (!response.success) {
this.snackBar.open('Authentication failed', 'Invalid email or password.', {
duration: 3000
});
}
});
}
}
O método createForm() define dois campos obrigatórios, e o e-mail ainda passa pelo validador de formato. Em seguida, o método login() envia os dados ao serviço e mostra um aviso com MatSnackBar quando a autenticação falha.
Veja como o fluxo fica até aqui, já com a validação e o redirecionamento funcionando:
Protegendo as rotas do login com Angular
Até aqui, nada impede alguém de digitar http://localhost:4200/ e acessar a Home sem autenticação. Ou seja, o login com Angular ainda não tem utilidade real, porque nada bloqueia o acesso direto pela URL.
Para resolver isso, você vai criar dois guards do tipo CanActivate. O primeiro manda para a Home quem já entrou, enquanto o segundo manda para o login quem ainda não entrou.
ng g guard services/guards/unauthenticated-user
ng g guard services/guards/authenticated-user
authenticated-user.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { UserService } from '../user.service';
@Injectable({
providedIn: 'root'
})
export class AuthenticatedUserGuard implements CanActivate {
constructor(private userService: UserService,
private router: Router) { }
canActivate() {
// Libera a rota apenas para quem tem token no storage
if (this.userService.isLoggedIn) {
return true;
}
this.router.navigate(['login']);
return false;
}
}
unauthenticated-user.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { UserService } from '../user.service';
@Injectable({
providedIn: 'root'
})
export class UnauthenticatedUserGuard implements CanActivate {
constructor(private userService: UserService,
private router: Router) { }
canActivate() {
// Quem já entrou não precisa ver a tela de login de novo
if (this.userService.isLoggedIn) {
this.router.navigate(['']);
return false;
}
return true;
}
}
Agora basta aplicar os guards no arquivo app-routing.module.ts:
const routes: Routes = [
{ path: 'login', component: LoginComponent, canActivate: [UnauthenticatedUserGuard] },
{
path: '', component: MainComponent, canActivate: [AuthenticatedUserGuard],
children: [
{ path: '', component: HomeComponent }
],
},
];
Como o guard está na rota pai, todas as rotas filhas herdam a proteção do login com Angular. Assim, qualquer página nova que você criar dentro do layout já nasce protegida.
Guards de classe em versões recentes
Este tutorial mantém os guards baseados em classe, padrão na versão do Angular usada no exemplo. Porém, a partir do Angular 15, a documentação marcou essa implementação como obsoleta em favor de guards funcionais.
Segundo o artigo da HeroDevs sobre guards funcionais, a nova forma usa funções e a função inject() para obter dependências. Mesmo assim, a lógica de decisão continua igual à que você viu aqui.
Implementando o logout no login com Angular
Para testar o fluxo inteiro, falta ligar o botão de logout ao serviço. Primeiro, adicione o evento de clique no botão do layout:
main.component.html
<button mat-icon-button (click)="logout()">
<mat-icon>logout</mat-icon>
</button>
Depois, no arquivo main.component.ts, injete o UserService e chame o método logout(). Esse método limpa o storage e devolve o usuário para a tela de login.
import { Component } from '@angular/core';
import { UserService } from '../../../services/user.service';
@Component({
selector: 'app-main',
templateUrl: './main.component.html',
styleUrls: ['./main.component.scss']
})
export class MainComponent {
constructor(private userService: UserService) { }
logout() {
this.userService.logout();
}
}
Para validar, tente abrir a rota raiz sem autenticação e, depois, tente abrir a rota de login já autenticado. Em ambos os casos, os guards devem redirecionar você para o lugar certo.
Com isso, o login com Angular já funciona de ponta a ponta no front end.
Enviando o token do login com Angular
Depois do login com Angular, cada chamada protegida para a API precisa levar o token do usuário. Esse token prova à API que o usuário já se autenticou e pode executar ações conforme suas permissões.
Se você quiser entender o formato desse token, o artigo o que é JWT explica a estrutura em detalhes. Do lado do servidor, o tutorial de autenticação e autorização com JWT e Bearer no ASP.NET 7 mostra como a API valida esse cabeçalho.
Para gerar o interceptor, use o comando ng g interceptor services/interceptors/token. O arquivo fica assim:
token.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
import { UserService } from '../user.service';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private userService: UserService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.userService.userToken;
const requestUrl: Array<any> = request.url.split('/');
const apiUrl: Array<any> = environment.apiUrl.split('/');
// Só adiciona o token quando a chamada vai para a nossa API
if (token && requestUrl[2] === apiUrl[2]) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
token: `${token}`
}
});
return next.handle(request).pipe(catchError(error => {
// 401: o token não vale mais, então encerramos a sessão
if (error instanceof HttpErrorResponse && error.status === 401) {
this.userService.logout();
}
return throwError(error);
}));
}
return next.handle(request);
}
}
Como o interceptor funciona
De forma resumida, o código acima executa os passos a seguir:
- Primeiro, lê o token do usuário que está no storage.
- Depois, pega a URL da requisição em andamento.
- Em seguida, obtém a
apiUrlconfigurada noenvironment. - Compara o host das duas URLs para saber se a chamada vai para a nossa API.
- Caso vá, adiciona o token no cabeçalho da requisição.
- Caso não vá, segue com a requisição original.
- Ao terminar, verifica se a resposta trouxe algum erro.
- Se o erro for 401 Unauthorized, encerra a sessão, porque o usuário não está mais autenticado na API.
Note que o interceptor usa request.clone() em vez de alterar a requisição. Isso acontece porque, segundo a documentação de interceptors do Angular, requisições e respostas são imutáveis e as alterações sempre passam por um clone.
Além disso, a comparação de host evita um problema sério no login com Angular. Sem ela, o token iria junto em chamadas para qualquer domínio externo que a aplicação acessasse.
Registrando o interceptor
Para fechar, registre o interceptor nos providers do app.module.ts, usando o token HTTP_INTERCEPTORS com multi: true:
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 { MainComponent } from './pages/shared/main/main.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
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,
MainComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
HttpClientModule,
ReactiveFormsModule,
FormsModule,
MatCardModule,
MatInputModule,
MatButtonModule,
MatSidenavModule,
MatToolbarModule,
MatIconModule,
MatSnackBarModule
],
providers: [
// multi: true permite registrar vários interceptors
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
A opção multi: true permite que vários interceptors convivam no mesmo token de injeção. Vale lembrar que, nesse modelo, eles rodam na ordem em que você registra os providers.
Conclusão
Neste tutorial, você construiu um login com Angular completo, com formulário reativo, logout, guards e envio automático do token. Cada peça tem uma responsabilidade clara, o que facilita manter e evoluir o código.
O storage guarda a sessão, os guards controlam o acesso às rotas e o interceptor cuida da comunicação com a API. Por fim, troque o mock pela chamada real assim que a sua API de autenticação estiver pronta.
A partir dessa base, você pode evoluir o login com Angular com refresh token, perfis de acesso e telas de cadastro.
O código completo do exemplo original está disponível no repositório AngularLoginExemplo no GitHub.
FAQ
Onde guardar o token em uma aplicação Angular?
O exemplo usa localStorage pela simplicidade. Em cenários mais sensíveis, avalie cookies httpOnly e secure, que o JavaScript da página não consegue ler.
Converter o token para Base64 deixa o login mais seguro?
Não. Base64 é apenas uma codificação reversível e serve só para deixar o valor menos legível no navegador.
Por que usar dois guards em vez de um?
Um guard protege as páginas internas de quem não entrou. O outro impede que um usuário já autenticado volte para a tela de login.
O interceptor envia o token para qualquer URL?
Não. Ele compara o host da requisição com a apiUrl do environment e só adiciona o cabeçalho nas chamadas para a sua API.
O que acontece quando a API responde 401?
O interceptor chama o logout(), limpa o storage e leva o usuário de volta para a tela de login.