Se você já usou um site como o Vercel ou Medium, provavelmente já experimentou um login sem senha antes.
O fluxo normalmente é assim: insira seu e-mail-> formulário de envio-> o e-mail é enviado para você-> você clica no link interno-> você está conectado.
É um fluxo bastante conveniente para todos. Os usuários não precisam se lembrar de uma senha com o conjunto de regras arbitrárias do site, e os webmasters (as pessoas ainda usam esse termo?) Não precisam se preocupar com vazamentos de senha ou se sua criptografia é boa o suficiente.
Neste artigo, vamos explorar como alguém pode implementar esse fluxo usando uma instalação padrão do Laravel.
Vamos assumir que você tem um entendimento prático da estrutura MVC do Laravel e que seu ambiente já possui o composer
e o php
configurados.
Por favor, note que os codeblocks neste artigo podem não incluir o arquivo inteiro para brevidade.
Configuração do ambiente
Vamos começar criando um novo aplicativo Laravel 8:
$ composer criar-projeto laravel/laravel magic-links
Então, precisamos cd
em nosso projeto e garantir que inserimos nossas credenciais de banco de dados. Certifique-se de criar o banco de dados com antecedência também.
No meu caso, estou usando PostgreSQL e faço toda a minha configuração por meio do TablePlus . Abra o arquivo .env
:
#.env DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=magic_link DB_USERNAME=postgres DB_PASSWORD=postgres
Agora nosso banco de dados está configurado, mas não execute as migrações ainda! Vamos dar uma olhada na migração de usuário padrão que o Laravel criou para nós em database/migrations/2014_10_12_000000_create_users_table.php
.
Você verá que a tabela de usuário padrão contém uma coluna para a senha. Como estamos fazendo autenticação sem senha, podemos nos livrar dela:
public function up () { Esquema:: criar ('usuários', função (Blueprint $ table) { $ tabela-> id (); $ tabela-> string ('nome'); $ tabela-> string ('email')-> exclusivo (); $ table-> timestamp ('email_verified_at')-> nullable (); $ table-> rememberToken (); $ table-> timestamps (); }); }
Vá em frente e salve o arquivo após excluir essa linha. Enquanto estamos limpando as coisas, vamos em frente e exclua a migração para a tabela de redefinição de senha, pois não será útil para nós:
$ rm database/migrations/2014_10_12_100000_create_password_resets_table.php
Nosso esquema de banco de dados inicial está pronto, então vamos executar nossas migrações:
$ php artisan migrate
Vamos também remover o atributo password
do array $ fillable
do modelo do usuário em app/Models/User.php
, uma vez que ele não existe mais:
protegido $ fillable=[ 'nome', 'o email', ];
Também queremos configurar nosso driver de e-mail para que possamos visualizar nossos e-mails de login. Eu gosto de usar Mailtrap que é um receptor SMTP gratuito (você pode enviar e-mails para qualquer endereço e eles só aparecerá no Mailtrap, não será entregue ao usuário real), mas você pode usar o que quiser.
Se você não quiser configurar nada, pode usar o mailer log
e os e-mails aparecerão em storage/logs/laravel.log
como brutos texto.
De volta ao mesmo arquivo .env
de antes:
#.env MAIL_MAILER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=redigido MAIL_PASSWORD=editado MAIL_ENCRYPTION=tls [email protected]
Agora estamos prontos para construir!
Nossa abordagem
Falamos sobre como é o fluxo da perspectiva do usuário no início deste artigo, mas como isso funciona do ponto de vista técnico?
Bem, dado um usuário, precisamos ser capazes de enviar a ele um link exclusivo que, ao clicar nele, o logue em sua própria conta.
Isso nos diz que provavelmente precisaremos gerar um token único de algum tipo, associá-lo ao usuário que está tentando fazer login, construir uma rota que examine esse token e determine se ele é válido e, em seguida, registra o usuário in. Também queremos permitir que esses tokens sejam usados apenas uma vez e só sejam válidos por um determinado período de tempo depois de gerados.
Como precisamos acompanhar se o token já foi ou não usado, vamos armazená-los no banco de dados. Também será útil controlar qual token pertence a qual usuário, bem como se o token foi usado ou não e se já expirou.
Crie um usuário de teste
Vamos nos concentrar apenas no fluxo de login neste artigo. Caberá a você criar uma página de registro, embora ela siga as mesmas etapas.
Por causa disso, precisaremos de um usuário no banco de dados para testar o login. Vamos criar um usando o tinker:
$ php artesão consertador > Usuário:: criar (['name'=>'Jane Doe','email'=>'[email protected]'])
A rota de login
Começaremos criando um controlador, AuthController
, que usaremos para lidar com a funcionalidade de login, verificação e logout:
$ php artisan make: controlador AuthController
Agora vamos registrar as rotas de login no arquivo routes/web.php
de nosso aplicativo. Abaixo da rota de boas-vindas, vamos definir um grupo de rotas que protegerá nossas rotas de autenticação usando o middleware guest
, impedindo que as pessoas já logadas as visualizem.
Dentro desse grupo, criaremos duas rotas. Um para mostrar a página de login, o outro para lidar com o envio do formulário. Também daremos nomes a eles para que possamos referenciá-los facilmente mais tarde:
Route:: group (['middleware'=> ['guest']], function () { Route:: get ('login', [AuthController:: class,'showLogin'])-> nome ('login.show'); Route:: post ('login', [AuthController:: class,'login'])-> nome ('login'); });
Agora as rotas estão registradas, mas precisamos criar as ações que irão responder a essas rotas. Vamos criar esses métodos no controlador que criamos app/Http/Controllers/AuthController.php
.
Por enquanto, nossa página de login retorna uma visualização localizada em auth.login
(que criaremos a seguir) e criaremos um método login
de espaço reservado que voltaremos quando criarmos nosso formulário:
php namespace App \ Http \ Controllers; use Illuminate \ Http \ Request; classe AuthController extends Controller { public function showLogin () { visualização de retorno ('auth.login'); } login de função pública (solicitação $ solicitação) { //PENDÊNCIA } }
Usaremos o sistema de templates do Laravel, Blade e TailwindCSS para nossas visualizações.
Como o foco principal deste artigo é a lógica de back-end, não entraremos em detalhes sobre o estilo. Não quero perder tempo definindo uma configuração CSS adequada, então usaremos isso TailwindCSS JIT CDN que podemos colocar em nosso layout que lidará com puxar os estilos certos.
Você pode notar um lampejo de estilos ao carregar a página pela primeira vez. Isso ocorre porque os estilos não existem até depois que a página é carregada. Em um ambiente de produção, você não iria querer isso, mas pelo bem do tutorial, é bom.
Vamos começar criando um layout geral que podemos usar para todas as nossas páginas. Este arquivo ficará em resources/views/layouts/app.blade.php
:
{{$ title}} @yield ('conteúdo')
Há algumas coisas que vou apontar aqui
- O título da página será definido por uma variável
$ title
que passaremos para o layout quando a estendermos - A
@yield ('content')
diretiva Blade-quando estendemos a partir deste layout, usaremos uma seção nomeada chamada “conteúdo” para colocar nosso conteúdo específico da página - O script TailwindCSS JIT CDN que usamos para processar nossos estilos
Agora que temos o layout, podemos criar a página de registro em resources/views/auth/login.blade.php
:
@extends ('layouts.app', ['title'=>'Login']) @section ('conteúdo')@endsectionLogin
Há algumas coisas acontecendo aqui, vamos apontar algumas coisas:
- Começamos estendendo o layout que criamos anteriormente e passando a ele o título de “Login”, que será o título da guia de nossos documentos
- Declaramos uma seção chamada
content
(lembra do@yield
anterior?) e colocamos o conteúdo da nossa página dentro, que será renderizado no layout - Alguns contêineres e estilos básicos são aplicados para centralizar o formulário no meio da tela
- A ação do formulário aponta para uma rota nomeada
route ('login')
que, se nos lembrarmos do arquivoroutes/web.php
, é o nome que demos à solicitação de loginPOST
em nosso controlador - Incluímos o campo CSRF oculto usando a diretiva
@csrf
( leia mais aqui ) - Nós condicionalmente mostramos quaisquer erros de validação fornecidos pelo Laravel usando a diretiva
@error
Se você carregar a página, ela deve se parecer com isto:
Bastante básico, pedimos apenas o e-mail do usuário. Se enviarmos o formulário agora, você verá apenas uma tela em branco porque nosso método de login
que definimos anteriormente está vazio. Vamos implementar o método login
em nosso AuthController
para enviar um link para concluir o login.
O fluxo será parecido com este: validar dados do formulário-> enviar link de login-> mostrar uma mensagem para o usuário de volta na página dizendo-lhe para verificar o e-mail.
//app/Http/Controllers/AuthController.php //perto de outras instruções de uso use App \ Models \ User; //dentro da classe login de função pública (solicitação $ solicitação) { $ data=$ request-> validate ([ 'email'=> ['obrigatório','email','existe: usuários, email'], ]); User:: whereEmail ($ data ['email'])-> first ()-> sendLoginLink (); sessão ()-> flash ('sucesso', verdadeiro); return redirect ()-> back (); }
Há algumas coisas que estamos fazendo aqui:
- Validando os dados do formulário-informando que o e-mail é obrigatório, deve ser um e-mail válido e existe em nosso banco de dados
- Encontramos o usuário pelo e-mail fornecido e chamamos uma função
sendLoginLink
que precisaremos implementar - Nós mostramos um valor para a sessão indicando que a solicitação foi bem-sucedida e, em seguida, retornamos o usuário de volta à página de login
Existem algumas tarefas incompletas nas etapas acima, então precisaremos implementá-las agora.
Começaremos atualizando nossa visualização de login para verificar esse booleano de sucesso, ocultando nosso formulário e mostrando ao usuário uma mensagem, se estiver presente. De volta a resources/views/auth/login.blade.php
:
@extends ('layouts.app', ['title'=>'Login']) @section ('conteúdo')@endsection@if (! sessão ()-> has ('sucesso'))Login
@senãoClique no link enviado para o seu e-mail para concluir o login.
@fim se
Aqui, simplesmente envolvemos o formulário em uma condicional.
Está dizendo:
- Acabamos de enviar um formulário?
- Não-mostre o formulário de registro em vez disso
- Sim-informe ao usuário que sua conta foi criada e verifique seu e-mail em busca de um link
Agora, se você enviar esse formulário novamente, verá um erro dizendo que precisamos implementar essa função sendLoginLink
no modelo Usuário
. Gosto de armazenar lógica assim no próprio modelo para que possamos reutilizá-la em nosso aplicativo mais tarde.
Abra app/Models/User.php
e crie um método vazio para preencher seu lugar:
public function sendLoginLink () { //PENDÊNCIA }
Agora, envie o formulário novamente e certifique-se de ver a mensagem de sucesso abaixo:
É claro que você ainda não recebeu um e-mail, mas agora podemos prosseguir para essa etapa.
Implementando a função sendLoginLink
Refletindo sobre a abordagem para tokens que discutimos acima, aqui está o que precisamos fazer agora:
- Gere um token exclusivo e anexe-o ao usuário
- Envie ao usuário um e-mail com um link para uma página que valide esse token
Vamos mantê-los em uma tabela chamada login_tokens
. Vamos criar o modelo e a migração (-m
):
$ php artisan make: model-m LoginToken
Para a migração, precisamos:
- Um token exclusivo para o url que estamos gerando
- Uma associação que o vincula ao usuário solicitante
- Uma data que indica quando o token expira
- Um sinalizador que nos informa se o token já foi ou não consumido. Usaremos um campo de carimbo de data/hora para isso, pois a ausência de um valor nesta coluna nos dirá se foi usado e, sendo um carimbo de data/hora, também nos informa quando foi consumido-vitória dupla!
Abra a migração que foi gerada e adicione as colunas necessárias:
Schema:: create ('login_tokens', function (Blueprint $ table) { $ tabela-> id (); $ table-> unsignedBigInteger ('user_id'); $ tabela-> estrangeiras ('id_do_usuário')-> referências ('id')-> on ('usuários')-> cascadeOnDelete (); $ tabela-> string ('token')-> exclusivo (); $ table-> timestamp ('consumed_at')-> nullable (); $ table-> timestamp ('expires_at'); $ table-> timestamps (); });
Certifique-se de executar a migração depois:
$ php artisan migrate
Em seguida, atualize nosso novo modelo app/Models/LoginToken
para considerar algumas coisas:
- Defina nossa propriedade
$ guarded
como uma matriz vazia, o que significa que não estamos restringindo quais colunas podem ser preenchidas - Crie uma propriedade
$ datas
que lançará nossos camposexpires_at
econsumed_at
para instâncias deCarbon \ Carbon
quando nós os referimos em código php para conveniência mais tarde - Nosso método
user ()
que nos permite fazer referência ao usuário associado ao token
classe LoginToken extends Model { use HasFactory; protegido $ guardado=[]; protegido $ datas=[ 'expires_at','consumed_at', ]; usuário de função pública () { return $ this-> belongsTo (User:: class); } }
Também é uma boa ideia colocar a associação inversa no modelo Usuário
:
//dentro de app/Models/User.php public function loginTokens () { return $ this-> hasMany (LoginToken:: class); }
Agora que configuramos o modelo, podemos realizar a primeira etapa de nossa função sendLoginLink ()
, que é a criação do token.
De volta a app/Models/User.php
, vamos criar o token para o usuário usando a nova associação loginTokens ()
que acabamos de criar e dar a ele uma string aleatória usando o ajudante Str
do Laravel e um prazo de 15 minutos a partir de agora.
Como definimos expires_at
e consumed_at
como datas no modelo LoginToken
, podemos simplesmente passar uma data fluente e ela será convertida adequadamente. Também faremos o hash do token antes de inseri-lo no banco de dados para que, se esta tabela fosse comprometida, ninguém pudesse ver os valores brutos do token.
Estamos usando um hash que pode ser reproduzido para que possamos procurá-lo novamente mais tarde, quando necessário:
use Illuminate \ Support \ Str; public function sendLoginLink () { $ plaintext=Str:: random (32); $ token=$ this-> loginTokens ()-> criar ([ 'token'=> hash ('sha256', $ plaintext), 'expires_at'=> now ()-> addMinutes (15), ]); //todo enviar e-mail }
Agora que temos um token, podemos enviar ao usuário um e-mail que contém um link com o token (texto simples) na url que validará sua sessão. O token precisa estar no URL para que possamos pesquisar a que usuário se destina.
Não queremos apenas usar o ID do LoginToken
porque então um usuário poderia ir um por um para encontrar um URL válido. Veremos outra forma de proteção contra isso mais tarde.
Comece criando a classe mailer que representará o e-mail:
$ php artisan make: mail MagicLoginLink
Abra o mailer gerado em app/Mail/MagicLoginLink.php
e digite o seguinte:
php namespace App \ Mail; use Illuminate \ Bus \ Queueable; use Illuminate \ Mail \ Mailable; use Illuminate \ Queue \ SerializesModels; use Illuminate \ Support \ Facades \ URL; classe MagicLoginLink extends Mailable { use Queueable, SerializesModels; public $ plaintextToken; public $ expiresAt; função pública __construct ($ plaintextToken, $ expiresAt) { $ this-> plaintextToken=$ plaintextToken; $ this-> expiresAt=$ expiresAt; } construção de função pública () { return $ this-> assunto ( config ('app.name').'Verificação de login' )-> markdown ('emails.magic-login-link', [ 'url'=> URL:: TemporarySignedRoute ('verificar-login', $ this-> expiresAt, [ 'token'=> $ this-> plaintextToken, ]), ]); } }
Aqui está o que está acontecendo-o remetente pegará o token de texto simples e a data de validade e os armazenará em propriedades públicas. Isso nos permitirá usá-lo posteriormente no método build ()
quando estiver sendo composto.
Dentro do método build ()
, estamos definindo o assunto do e-mail e dizendo a ele para procurar por uma visualização formatada com markdown dentro de resources/views/emails/magic-login-link.blade.php
. O Laravel fornece um estilo padrão para e-mails de markdown, dos quais tiraremos vantagem em um momento.
Também passamos uma variável url
para a visualização que será o link em que o usuário clica.
Essa propriedade url
é um url assinado temporário . Ele leva em uma rota nomeada, uma data de expiração (que queremos ser a expiração de nossos tokens) e quaisquer parâmetros (neste caso, token
sendo a string aleatória sem hash que geramos). Uma URL assinada garante que a URL não foi modificada por meio do hash da URL com um segredo que apenas o Laravel conhece.
Embora vamos adicionar verificações em nossa rota verificar-login
para garantir que nosso token ainda seja válido (com base em expires_at
e consumed_at
properties), assinar o URL nos dá segurança extra no nível da estrutura, já que ninguém será capaz de forçar bruta a rota verify-login
com tokens aleatórios para ver se eles podem encontrar um que os registre pol.
Agora precisamos implementar essa visualização de redução em resources/views/emails/magic-login-link.blade.php
. Você deve estar se perguntando por que a extensão é .blade.php
. Isso ocorre porque, embora estejamos escrevendo markdown neste arquivo, podemos usar diretivas Blade dentro para construir componentes reutilizáveis que podemos usar em nossos e-mails.
O Laravel nos fornece componentes pré-estilizados fora da caixa para começarmos imediatamente. Estamos usando mail:: message
, que nos dá um layout e uma frase de chamariz via mail:: button
:
@component ('mail:: mensagem') Olá, para terminar o login clique no link abaixo @component ('mail:: button', ['url'=> $ url]) Clique para entrar @endcomponent @endcomponent
Agora que construímos o conteúdo do e-mail, podemos concluir o método sendLoginLink ()
enviando o e-mail. Vamos usar a fachada Mail
fornecida pelo Laravel para especificar os e-mails dos usuários para os quais estamos enviando, e que o conteúdo do e-mail deve ser criado a partir do MagicLoginLink
class que acabamos de configurar.
Também usamos queue ()
em vez de send ()
para que o e-mail seja enviado em segundo plano em vez de durante a solicitação atual. Certifique-se de ter seu driver de fila configurado apropriadamente ou de que está usando o driver sync
(este é o padrão) se quiser que isso aconteça imediatamente.
De volta a app/Models/User.php
:
use Illuminate \ Support \ Facades \ Mail; use App \ Mail \ MagicLoginLink; public function sendLoginLink () { $ plaintext=Str:: random (32); $ token=$ this-> loginTokens ()-> criar ([ 'token'=> hash ('sha256', $ plaintext), 'expires_at'=> now ()-> addMinutes (15), ]); Mail:: to ($ this-> email)-> queue (new MagicLoginLink ($ plaintext, $ token-> expires_at)); }
Se você enviasse nosso formulário de login, veria agora um e-mail parecido com este:
A rota de verificação
Se você tentou clicar no link, provavelmente recebeu um erro 404. Isso porque, em nosso e-mail, enviamos ao usuário um link para a rota nomeada verify-login
, mas ainda não a criamos!
Registre a rota no grupo de rotas em routes/web.php
:
Route:: group (['middleware'=> ['guest']], function () { Route:: get ('login', [AuthController:: class,'showLogin'])-> nome ('login.show'); Route:: post ('login', [AuthController:: class,'login'])-> nome ('login'); Route:: get ('verify-login/{token}', [AuthController:: class,'verifyLogin'])-> nome ('verify-login'); });
E criaremos a implementação dentro de nossa classe AuthController
por meio de um método verifyLogin
:
public function verifyLogin (Request $ request, $ token) { $ token=\ App \ Models \ LoginToken:: whereToken (hash ('sha256', $ token))-> firstOrFail (); abort_unless ($ request-> hasValidSignature () && $ token-> isValid (), 401); $ token-> consumir (); Auth:: login ($ token-> usuário); return redirect ('/'); }
Aqui, estamos fazendo o seguinte:
-
-
- Encontrar o token fazendo hash do valor do texto simples e comparando-o com a versão em hash em nosso banco de dados (gera 404 se não for encontrado-via
firstOrFail ()
) - Abortar a solicitação com um código de status 401 se o token for inválido ou o URL assinado for inválido (você pode se divertir aqui se quiser mostrar uma visualização ou algo que permita ao usuário saber mais informações, mas por causa de neste tutorial iremos apenas eliminar o pedido)
- Marcando o token como usado para que não possa ser usado novamente
- Login do usuário associado ao token
- Redirecionando-os para a página inicial
- Encontrar o token fazendo hash do valor do texto simples e comparando-o com a versão em hash em nosso banco de dados (gera 404 se não for encontrado-via
-
Chamamos alguns métodos no token que ainda não existem, então vamos criá-los:
-
-
-
isValid ()
será verdadeiro se o token ainda não tiver sido consumido (consumed_at===null
) e se não tiver expirado (expires_at <=agora
) - Vamos extrair o expirado e o consumido, verificando suas próprias funções para torná-lo mais legível
-
consume ()
vai definir a propriedadeconsumed_at
para o carimbo de data/hora atual
-
-
Gosto de encapsular essa lógica no modelo diretamente para que seja fácil de ler e reutilizar. Abra app/Models/LoginToken.php
:
função pública isValid () { return! $ this-> isExpired () &&! $ this-> isConsumed (); } função pública isExpired () { return $ this-> expires_at-> isBefore (now ()); } public function isConsumed () { return $ this-> consumed_at!==null; } função pública consumir () { $ this-> consumed_at=now (); $ this-> save (); }
Se você clicar naquele link de login do seu e-mail agora, deverá ser redirecionado para a rota /
!
Você também perceberá que, se clicar no link novamente, a tela de erro será exibida porque agora ele é inválido.
Toques finais
Agora que nosso fluxo de autenticação está funcionando, vamos proteger nossa rota raiz para ser visualizada apenas por aqueles que estão logados e adicionar uma maneira de sair para que possamos fazer o fluxo novamente.
Para começar, edite a rota raiz padrão em app/web.php
para adicionar o middleware auth
:
Route:: get ('/', function () { visualização de retorno ('bem-vindo'); })-> middleware ('auth');
Vamos também ajustar a visualização padrão de boas-vindas para mostrar um pouco de informação sobre nosso usuário conectado, bem como fornecer um link para sair. Substitua o conteúdo de resources/views/welcome.blade.php
pelo seguinte:
@extends ('layouts.app', ['title'=>'Home']) @section ('conteúdo')@endsectionConectado como {{Auth:: user ()-> name}}
Sair
E por último a rota de logout que esquecerá nossa sessão e nos levará de volta à tela de login. Abra routes/web.php
novamente e adicione esta rota ao final do arquivo:
Route:: get ('logout', [AuthController:: class,'logout'])-> nome ('logout');
E, finalmente, precisamos implementar a ação de logout em nosso AuthController
:
saída de função pública () { Auth:: logout (); retorno de redirecionamento (rota ('login')); }
Agora, sua página inicial deve ter esta aparência e ser visualizada apenas por aqueles que estão logados:
Conclusão
That’s a wrap! We covered a lot of ground but you’ll notice the overall code we wrote is pretty low for a feature like this. I hope you learned a trick or two along the way.
Full source code can be viewed here.
The post Magic login links with Laravel appeared first on LogRocket Blog.