Hoje veremos como é fácil integrar Token da web JSON (JWT) autenticação em seu Angular 6 (ou posterior) aplicativo de página única (SPA). Vamos começar com um pouco de fundo.
A resposta mais fácil e concisa aqui é que eles são convenientes, compactos e seguros. Vejamos essas afirmações em detalhes:
Tudo isso se resume a que você tem uma maneira segura e eficiente de autenticar usuários e, em seguida, verificar as chamadas para seus terminais de API sem ter que analisar nenhuma estrutura de dados nem implementar sua própria criptografia.
Então, com um pouco de conhecimento, agora podemos mergulhar em como isso funcionaria em um aplicativo real. Para este exemplo, vou assumir que temos um servidor Node.js hospedando nossa API, e estamos desenvolvendo um SPA toda a lista usando Angular 6. Vamos trabalhar também com esta estrutura de API:
/auth
→ POST
(poste nome de usuário e senha para autenticar e receber de volta um JWT) /todos
→ GET
(retornar uma lista de itens da lista de tarefas para o usuário) /todos/{id}
→ GET
(retornar um item específico da lista de tarefas) /users
→ GET
(retorna uma lista de usuários) Faremos a criação deste aplicativo simples em breve, mas por enquanto, vamos nos concentrar na interação em teoria. Temos uma página de login simples, onde o usuário pode inserir seu nome de usuário e senha. Quando o formulário é enviado, ele envia essas informações para o /auth
ponto final. O servidor Node pode então autenticar o usuário da maneira apropriada (pesquisa de banco de dados, consulta de outro serviço da web etc.), mas, em última análise, o terminal precisa retornar um JWT.
O JWT para este exemplo conterá alguns reivindicações reservadas , e alguns reivindicações privadas . As declarações reservadas são simplesmente pares de valores-chave recomendados pelo JWT comumente usados para autenticação, enquanto as declarações privadas são pares de valores-chave aplicáveis apenas ao nosso aplicativo:
Reivindicações reservadas
iss
: Emissor deste token. Normalmente o FQDN do servidor, mas pode ser qualquer coisa, desde que o aplicativo cliente saiba que o espera.exp
: Data e hora de expiração deste token. Isto é em segundos desde a meia-noite de 01 de janeiro de 1970 GMT (horário Unix). nbf
: Não é válido antes do carimbo de data / hora. Não é usado com frequência, mas fornece um limite inferior para a janela de validade. Mesmo formato que exp
. Reivindicações privadas
uid
: ID do usuário do usuário conectado.role
: Função atribuída ao usuário conectado.Nossas informações serão codificadas em base64 e assinadas usando HMAC com a chave compartilhada todo-app-super-shared-secret
. Abaixo está um exemplo de como o JWT se parece:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ
este string é tudo o que precisamos para ter certeza de que temos um login válido, para saber qual usuário está conectado e até mesmo qual (is) função (ões) o usuário tem.
A maioria das bibliotecas e aplicativos armazenam este JWT em localStorage
ou sessionStorage
para fácil recuperação, mas isso é apenas uma prática comum. O que você faz com o token é com você, contanto que você possa fornecê-lo para chamadas de API futuras.
Agora, sempre que o SPA quiser fazer uma chamada para qualquer um dos endpoints de API protegidos, ele simplesmente precisa enviar junto com o token no Authorization
Cabeçalho HTTP.
Authorization: Bearer {JWT Token}
Nota : Mais uma vez, esta é uma prática comum. O JWT não prescreve nenhum método específico para enviar a si mesmo ao servidor. Você também pode anexá-lo ao URL ou enviá-lo em um cookie.
Depois que o servidor recebe o JWT, ele pode decodificá-lo, garantir a consistência usando o segredo compartilhado HMAC e verificar a expiração usando exp
e nbf
Campos. Ele também pode usar o iss
campo para garantir que era a parte emissora original deste JWT.
Assim que o servidor estiver satisfeito com a validade do token, as informações armazenadas no JWT podem ser usadas. Por exemplo, o uid
que incluímos nos dá o ID do usuário que fez a solicitação. Para este exemplo específico, também incluímos role
, que nos permite tomar decisões sobre se o usuário deve ser capaz de acessar um determinado endpoint ou não. (Se você confia nessas informações ou deseja fazer uma pesquisa no banco de dados depende do nível de segurança necessário.)
function getTodos(jwtString) { var token = JWTDecode(jwtstring); if( Date.now() token.exp*1000) { throw new Error('Token has expired'); } if( token.iss != 'todoapi') { throw new Error('Token not issued here'); } var userID = token.uid; var todos = loadUserTodosFromDB(userID); return JSON.stringify(todos); }
Para acompanhar, você precisará ter uma versão recente do Node.js (6.x ou posterior), npm (3.x ou posterior) e angular-cli instalados. Se você precisar instalar o Node.js, que inclui npm, siga as instruções Aqui . Depois angular-cli
pode ser instalado usando npm
(ou yarn
, se você o instalou):
# installation using npm npm install -g @angular/cli # installation using yarn yarn global add @angular/cli
Não vou entrar em detalhes sobre o boilerplate do Angular 6 que usaremos aqui, mas para a próxima etapa, criei um repositório Github para conter um pequeno aplicativo de tarefas para ilustrar a simplicidade de adicionar autenticação JWT ao seu aplicativo. Basta cloná-lo usando o seguinte:
git clone https://github.com/sschocke/angular-jwt-todo.git cd angular-jwt-todo git checkout pre-jwt
O git checkout pre-jwt
o comando muda para uma versão nomeada onde o JWT não foi implementado.
Deve haver duas pastas dentro, chamadas server
e client
. O servidor é um servidor Node API que hospedará nossa API básica. O cliente é nosso aplicativo Angular 6.
Para começar, instale as dependências e inicie o servidor API.
cd server # installation using npm npm install # or installation using yarn yarn node app.js
Você deve ser capaz de seguir esses links e obter uma representação JSON dos dados. Por enquanto, até que tenhamos a autenticação, codificamos o /todos
endpoint para retornar as tarefas para userID=1
:
userID=1
Para começar com o aplicativo cliente, também precisamos instalar as dependências e iniciar o servidor de desenvolvimento.
cd client # using npm npm install npm start # using yarn yarn yarn start
Nota : Dependendo da velocidade da sua linha, pode demorar um pouco para baixar todas as dependências.
Se tudo estiver indo bem, você deve ver algo assim ao navegar para http: // localhost: 4200 :
Para adicionar suporte à autenticação JWT, usaremos algumas bibliotecas padrão disponíveis que a tornam mais simples. Você pode, é claro, renunciar a essas conveniências e implementar tudo sozinho, mas isso está além do nosso escopo aqui.
este tipo de equilíbrio apresenta elementos que vêm de um ponto central ou criam um foco central.
Primeiro, vamos instalar uma biblioteca no lado do cliente. É desenvolvido e mantido por Auth0 , que é uma biblioteca que permite adicionar autenticação baseada em nuvem para um site. Utilizar a biblioteca em si não requer o uso de seus serviços.
cd client # installation using npm npm install @auth0/angular-jwt # installation using yarn yarn add @auth0/angular-jwt
Chegaremos ao código em um segundo, mas enquanto estamos nisso, vamos configurar o lado do servidor também. Usaremos body-parser
, jsonwebtoken
e express-jwt
bibliotecas para fazer o Node entender os corpos JSON POST e JWTs.
cd server # installation using npm npm install body-parser jsonwebtoken express-jwt # installation using yarn yarn add body-parser jsonwebtoken express-jwt
Primeiro, precisamos de uma maneira de autenticar os usuários antes de fornecer um token. Para nossa demonstração simples, vamos apenas configurar um endpoint de autenticação fixo com um nome de usuário e senha embutidos em código. Isso pode ser tão simples ou tão complexo quanto seu aplicativo requer. O importante é enviar um JWT de volta.
Em server/app.js
adicione uma entrada abaixo da outra require
linhas da seguinte forma:
const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const expressJwt = require('express-jwt');
Bem como o seguinte:
app.use(bodyParser.json()); app.post('/api/auth', function(req, res) { const body = req.body; const user = USERS.find(user => user.username == body.username); if(!user || body.password != 'todo') return res.sendStatus(401); var token = jwt.sign({userID: user.id}, 'todo-app-super-shared-secret', {expiresIn: '2h'}); res.send({token}); });
Este é principalmente um código JavaScript básico. Obtemos o corpo JSON que foi passado para o /auth
endpoint, encontre um usuário que corresponda a esse nome de usuário, verifique se temos um usuário e a senha correspondam e retorne um 401 Unauthorized
Erro HTTP se não.
A parte importante é a geração de tokens, e vamos dividir isso por seus três parâmetros. A sintaxe para sign
é o seguinte: jwt.sign(payload, secretOrPrivateKey, [options, callback])
, onde:
payload
é um objeto literal de pares de valores-chave que você gostaria de codificar em seu token. Essas informações podem ser decodificadas do token por qualquer pessoa que possua a chave de descriptografia. Em nosso exemplo, codificamos o user.id
para que, quando recebermos o token novamente no back-end para autenticação, saibamos com qual usuário estamos lidando.secretOrPrivateKey
é uma chave secreta compartilhada de criptografia HMAC - isso é o que usamos em nosso aplicativo, para simplificar - ou uma chave privada de criptografia RSA / ECDSA.options
representa uma variedade de opções que podem ser passadas para o codificador na forma de pares de valores-chave. Normalmente, especificamos pelo menos expiresIn
(torna-se exp
reivindicação reservada) e issuer
(iss
reivindicação reservada) para que um token não seja válido para sempre e o servidor possa verificar se ele de fato emitiu o token originalmente.callback
é uma função a ser chamada após a codificação ser concluída, caso se deseje manipular a codificação do token de forma assíncrona. (Você também pode ler sobre mais detalhes em options
e como usar criptografia de chave pública em vez de uma chave secreta compartilhada .)
Para fazer o Angular 6 funcionar com nosso JWT é bastante simples usando angular-jwt
. Basta adicionar o seguinte a client/src/app/app.modules.ts
:
import { JwtModule } from '@auth0/angular-jwt'; // ... export function tokenGetter() { return localStorage.getItem('access_token'); } @NgModule({ // ... imports: [ BrowserModule, AppRoutingModule, HttpClientModule, FormsModule, // Add this import here JwtModule.forRoot({ config: { tokenGetter: tokenGetter, whitelistedDomains: ['localhost:4000'], blacklistedRoutes: ['localhost:4000/api/auth'] } }) ], // ... }
Isso é basicamente tudo o que é necessário. Claro, temos mais alguns códigos para adicionar para fazer a autenticação inicial, mas o angular-jwt
A biblioteca se encarrega de enviar o token junto com cada solicitação HTTP.
tokenGetter()
A função faz exatamente o que diz, mas a forma como é implementada depende inteiramente de você. Optamos por retornar o token que salvamos em localStorage
. É claro que você é livre para fornecer qualquer outro método que desejar, desde que ele retorne o Token da web JSON string codificada.whiteListedDomains
existe a opção para que você possa restringir para quais domínios o JWT é enviado, de forma que APIs públicas não recebam seu JWT também.blackListedRoutes
opção permite que você especifique rotas específicas que não devem receber o JWT, mesmo se estiverem em um domínio na lista de permissões. Por exemplo, o endpoint de autenticação não precisa recebê-lo porque não há nenhum ponto: o token normalmente é nulo quando é chamado de qualquer maneira.Neste ponto, temos uma maneira de gerar um JWT para um determinado usuário usando o /auth
endpoint em nossa API, e temos o encanamento feito no Angular para enviar um JWT com cada solicitação HTTP. Ótimo, mas você pode apontar que absolutamente nada mudou para o usuário. E você estaria correto. Ainda podemos navegar para todas as páginas em nosso aplicativo e podemos chamar qualquer endpoint de API sem nem mesmo enviar um JWT. Não é bom!
Precisamos atualizar nosso aplicativo cliente para nos preocupar com quem está conectado e também atualizar nossa API para exigir um JWT. Vamos começar.
Precisaremos de um novo componente Angular para fazer login. Para ser breve, vou manter isso o mais simples possível. Também precisaremos de um serviço que atenderá a todos os nossos requisitos de autenticação e um Guarda Angular para proteger as rotas que não devem ser acessíveis antes de fazer o login. Faremos o seguinte no contexto do aplicativo cliente.
cd client ng g component login --spec=false --inline-style ng g service auth --flat --spec=false ng g guard auth --flat --spec=false
Isso deveria ter gerado quatro novos arquivos no client
pasta:
src/app/login/login.component.html src/app/login/login.component.ts src/app/auth.service.ts src/app/auth.guard.ts
Em seguida, precisamos fornecer o serviço de autenticação e proteção para nosso aplicativo. Atualizar client/src/app/app.modules.ts
:
import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; // ... providers: [ TodoService, UserService, AuthService, AuthGuard ],
E então atualize o roteamento em client/src/app/app-routing.modules.ts
para fazer uso da proteção de autenticação e fornecer uma rota para o componente de login.
// ... import { LoginComponent } from './login/login.component'; import { AuthGuard } from './auth.guard'; const routes: Routes = [ { path: 'todos', component: TodoListComponent, canActivate: [AuthGuard] }, { path: 'users', component: UserListComponent, canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, // ...
Finalmente, atualize client/src/app/auth.guard.ts
com o seguinte conteúdo:
import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router) { } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (localStorage.getItem('access_token')) { return true; } this.router.navigate(['login']); return false; } }
Para nosso aplicativo de demonstração, estamos simplesmente verificando a existência de um JWT no armazenamento local. Em aplicativos do mundo real, você decodificaria o token e verificaria sua validade, expiração, etc. Por exemplo, você poderia usar JwtHelperService por esta.
Nesse ponto, nosso aplicativo Angular agora sempre redirecionará você para a página de login, pois não temos como fazer login. Vamos corrigir isso, começando com o serviço de autenticação em client/src/app/auth.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class AuthService { constructor(private http: HttpClient) { } login(username: string, password: string): Observable { return this.http.post('/api/auth', {username: username, password: password}) .pipe( map(result => { localStorage.setItem('access_token', result.token); return true; }) ); } logout() { localStorage.removeItem('access_token'); } public get loggedIn(): boolean { return (localStorage.getItem('access_token') !== null); } }
Nosso serviço de autenticação tem apenas duas funções, login
e logout
:
login
POST
s fornecido username
e password
ao nosso backend e define o access_token
em localStorage
se receber um de volta. Por razões de simplicidade, não há tratamento de erros aqui. logout
simplesmente limpa access_token
de localStorage
, exigindo que um novo token seja adquirido antes que qualquer outra coisa possa ser acessada novamente.loggedIn
é uma propriedade booleana que podemos usar rapidamente para determinar se o usuário está logado ou não.E, por último, o componente de login. Eles não têm nenhuma relação com o trabalho real com o JWT, então fique à vontade para copiar e colar em client/src/app/login/login.components.html
:
{{error}}
Username Password Login
E client/src/app/login/login.components.ts
vai precisar:
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; import { first } from 'rxjs/operators'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { public username: string; public password: string; public error: string; constructor(private auth: AuthService, private router: Router) { } public submit() { this.auth.login(this.username, this.password) .pipe(first()) .subscribe( result => this.router.navigate(['todos']), err => this.error = 'Could not authenticate' ); } }
Pronto, nosso exemplo de login do Angular 6:
Nesta fase, devemos ser capazes de fazer login (usando jemma
, paul
ou sebastian
com a senha todo
) e ver todas as telas novamente. Mas nosso aplicativo mostra os mesmos cabeçalhos de navegação e nenhuma maneira de fazer logout, independentemente do estado atual. Vamos consertar isso antes de prosseguirmos para consertar nossa API.
Em client/src/app/app.component.ts
, substitua o arquivo inteiro pelo seguinte:
import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private auth: AuthService, private router: Router) { } logout() { this.auth.logout(); this.router.navigate(['login']); } }
E para client/src/app/app.component.html
substitua a seção pelo seguinte:
Todo List Users Login Logout
Tornamos nossa navegação ciente do contexto de que ela deve exibir apenas alguns itens, dependendo se o usuário está logado ou não. auth.loggedIn
pode, é claro, ser usado em qualquer lugar onde você possa importar o serviço de autenticação.
Você pode estar pensando, isso é ótimo ... tudo parece estar funcionando maravilhosamente . Mas tente fazer login com todos os três nomes de usuário diferentes e você notará algo: todos eles retornam a mesma lista de tarefas. Se dermos uma olhada em nosso servidor API, podemos ver que cada usuário, de fato, tem sua própria lista de itens, então o que há?
Bem, lembre-se de quando começamos, codificamos nosso /todos
Endpoint da API para sempre retornar a lista de tarefas para userID=1
. Isso acontecia porque não tínhamos como saber quem era o usuário conectado no momento.
Agora, vamos ver como é fácil proteger nossos endpoints e usar as informações codificadas no JWT para fornecer a identidade do usuário necessária. Inicialmente, adicione esta linha ao seu server/app.js
arquivo logo abaixo do último app.use()
ligar:
app.use(expressJwt({secret: 'todo-app-super-shared-secret'}).unless({path: ['/api/auth']}));
Usamos o express-jwt
middleware, diga a ele qual é o segredo compartilhado e especifique uma matriz de caminhos para os quais ele não deve exigir um JWT. E é isso. Não há necessidade de tocar em cada ponto de extremidade, crie if
todas as declarações, ou qualquer coisa.
Internamente, o middleware está fazendo algumas suposições. Por exemplo, ele assume que Authorization
O cabeçalho HTTP segue o padrão JWT comum de Bearer {token}
. (A biblioteca tem muitas opções para personalizar como funciona se esse não for o caso. Veja Express-jwt Usage para mais detalhes.)
Nosso segundo objetivo é usar as informações codificadas do JWT para descobrir quem está fazendo a chamada. Mais uma vez express-jwt
vem ao resgate. Como parte da leitura e verificação do token, ele define a carga útil codificada que enviamos no processo de assinatura para a variável req.user
no Express. Podemos então usá-lo para acessar imediatamente qualquer uma das variáveis que armazenamos. Em nosso caso, definimos userID
igual ao ID do usuário autenticado e, como tal, podemos usá-lo diretamente como req.user.userID
.
Atualizar server/app.js
novamente e altere o /todos
endpoint para ler da seguinte forma:
res.send(getTodos(req.user.userID));
E é isso. Nossa API agora está protegida contra acesso não autorizado e podemos determinar com segurança quem é nosso usuário autenticado em qualquer terminal. Nosso aplicativo cliente também tem um processo de autenticação simples, e qualquer serviço HTTP que escrevermos que chame nosso endpoint de API terá automaticamente um token de autenticação anexado.
Se você clonou o repositório Github e simplesmente deseja ver o resultado final em ação, pode verificar o código em sua forma final usando:
git checkout with-jwt
Espero que você tenha achado este passo a passo valioso para adicionar Autenticação JWT para seus próprios aplicativos Angular. Obrigado por ler!
este é o nome que se refere ao espaço vazio ao redor do tipo ou outros recursos em um layout.Relacionado: Tutorial JSON Web Token: Um Exemplo no Laravel e AngularJS
Em termos de programação, um token tem muitas definições. Em relação à segurança e autenticação, é simplesmente uma string opaca que codifica uma pequena quantidade de informação que pode ser facilmente transmitida e armazenada, mas com quase nenhuma chance de colisões com qualquer outro token.
A autenticação baseada em token significa simplesmente codificar os dados necessários para autenticar e autorizar um usuário em um token e, em seguida, usar esse token assinado criptograficamente para autorização em vez de combinações de nome de usuário / senha ao acessar recursos protegidos.
JWT é uma abreviatura de JSON Web Token, o que basicamente significa que é um objeto JSON com cabeçalho, carga útil e assinatura. Ele representa uma maneira segura de trocar informações de autenticação entre duas partes em uma rede, quando usado em conjunto com outras tecnologias, como SSL.
OAuth 2.0 é um protocolo padrão para executar a autorização em um grande número de plataformas suportadas. Por exemplo, Google, Facebook e OpenID usam OAuth 2.0. Ele define as etapas e o formato a seguir para autenticar com o servidor de autenticação fornecido e obter acesso aos recursos protegidos.