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:

 

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') 

Login

@csrf
@error ('email')

{{$ message}}

@enderror
@endsection

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 arquivo routes/web.php , é o nome que demos à solicitação de login POST 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:

Captura de tela da página do Laravel com caixa de login simples

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') 
@if (! sessão ()-> has ('sucesso'))

Login

@csrf
@error ('email')

{{$ message}}

@enderror
@senão

Clique no link enviado para o seu e-mail para concluir o login.

@fim se
@endsection

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:

Captura de tela da página do Laravel que diz

É 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:

  1. Gere um token exclusivo e anexe-o ao usuário
  2. 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 campos expires_at e consumed_at para instâncias de Carbon \ 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:

  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:

Captura de tela de um aplicativo Laravel que lê

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

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 propriedade consumed_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') 

Conectado como {{Auth:: user ()-> name}}

Sair
@endsection

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:

Screenshot of a Laravel web app that reads

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.