Programação Por Equipe DevBras

Capa do artigo: Dois logins no Laravel com guards: cliente e equipe sem misturar a sessão

Dois logins no Laravel com guards: cliente e equipe sem misturar a sessão

Configure um guard e um provider para cada tipo de usuário, proteja as rotas com auth:guard, separe a recuperação de senha e faça o logout de um sem derrubar o outro.

A analista de suporte da Torra Fina, loja fictícia, recupera a senha da área interna e cai na tela "Meus pedidos" de cliente. Dias depois, um cliente com o mesmo e-mail de um funcionário pede redefinição de senha e o link chega para a conta errada. Os dois problemas têm a mesma origem: uma tabela users só, com uma coluna is_admin e um if no controller. A resposta curta: no Laravel, dê a cada tipo de pessoa o seu guard, o seu provider, a sua tabela e o seu broker de senha. As rotas internas exigem auth:equipe, as do cliente exigem auth:web, e uma sessão não abre a porta da outra.

Os exemplos usam a estrutura do Laravel 11 e 12 (configuração de middleware em bootstrap/app.php). A ideia vale para versões anteriores, mudando o lugar onde alguns trechos ficam.

Por que um usuário com flag não basta

Cliente e funcionário usam e-mail e senha, mas não são a mesma entidade. Têm cadastros diferentes, regras de senha diferentes, telas de login diferentes e, muitas vezes, campos de entrada diferentes: o cliente entra com e-mail, a equipe com matrícula. Colocar todos na mesma tabela obriga cada parte do sistema a lembrar de conferir a flag. Basta esquecer em um lugar para um cliente ver o que não devia.

No Laravel, a autenticação tem duas peças. O guard define como a pessoa é lembrada entre requisições (na sessão, por exemplo). O provider define de onde ela vem (qual modelo, qual tabela). Com dois pares de guard e provider, você tem dois mundos que não se enxergam. O post dois logins no mesmo sistema discute essa decisão de forma independente de framework.

Como configurar os dois logins, passo a passo

  1. Crie a tabela e o modelo da equipe, separados de users.
  2. Declare o guard e o provider equipe em config/auth.php.
  3. Declare um broker de senha próprio, com tabela de tokens própria.
  4. Agrupe as rotas internas com guest:equipe e auth:equipe.
  5. Escreva o login da equipe usando Auth::guard('equipe').
  6. Ensine o Laravel para onde mandar visitantes de cada área.
  7. Faça o logout de um guard sem destruir a sessão inteira, se cliente e equipe puderem estar logados no mesmo navegador.

Modelo e configuração

// app/Models/Funcionario.php
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class Funcionario extends Authenticatable
{
    use Notifiable;

    protected $table = 'funcionarios';
    protected $fillable = ['nome', 'matricula', 'email', 'password'];
    protected $hidden = ['password', 'remember_token'];

    protected function casts(): array
    {
        return ['password' => 'hashed'];
    }
}
// config/auth.php (trechos)
'guards' => [
    'web' => ['driver' => 'session', 'provider' => 'users'],
    'equipe' => ['driver' => 'session', 'provider' => 'funcionarios'],
],

'providers' => [
    'users' => ['driver' => 'eloquent', 'model' => App\Models\User::class],
    'funcionarios' => ['driver' => 'eloquent', 'model' => App\Models\Funcionario::class],
],

'passwords' => [
    'users' => [
        'provider' => 'users',
        'table' => 'password_reset_tokens',
        'expire' => 60,
        'throttle' => 60,
    ],
    'funcionarios' => [
        'provider' => 'funcionarios',
        'table' => 'funcionario_password_reset_tokens',
        'expire' => 30,
        'throttle' => 60,
    ],
],

A tabela de tokens separada não é capricho. A tabela padrão usa o e-mail como chave; se um cliente e um funcionário tiverem o mesmo e-mail, os tokens de um sobrescrevem os do outro. Crie funcionario_password_reset_tokens com a mesma estrutura da padrão (email, token, created_at).

Rotas e login da equipe

// routes/web.php
use App\Http\Controllers\Equipe\LoginController;
use App\Http\Controllers\Equipe\PainelController;

Route::prefix('equipe')->name('equipe.')->group(function () {
    Route::middleware('guest:equipe')->group(function () {
        Route::get('login', [LoginController::class, 'create'])->name('login');
        Route::post('login', [LoginController::class, 'store']);
    });

    Route::middleware('auth:equipe')->group(function () {
        Route::get('/', PainelController::class)->name('painel');
        Route::post('logout', [LoginController::class, 'destroy'])->name('logout');
    });
});
// app/Http/Controllers/Equipe/LoginController.php (métodos store e destroy)
public function store(Request $request)
{
    $credenciais = $request->validate([
        'matricula' => ['required', 'string'],
        'password' => ['required', 'string'],
    ]);

    if (! Auth::guard('equipe')->attempt($credenciais, $request->boolean('lembrar'))) {
        throw ValidationException::withMessages([
            'matricula' => 'Matrícula ou senha inválidas.',
        ]);
    }

    $request->session()->regenerate();

    return redirect()->route('equipe.painel');
}

public function destroy(Request $request)
{
    Auth::guard('equipe')->logout();
    $request->session()->regenerateToken();

    return redirect()->route('equipe.login');
}

O attempt do provider Eloquent busca o registro pelos campos que não são senha, então matricula funciona sem configuração extra, e confere a senha com o hash. A mensagem de erro não diz qual dos dois campos falhou, pelo motivo explicado em login que diz "usuário não existe". O regenerate troca o id da sessão no login e evita fixação de sessão.

Repare no logout: ele chama Auth::guard('equipe')->logout() e regenera o token CSRF, mas não chama session()->invalidate(). Os dois guards usam a mesma sessão do navegador, cada um com sua chave. Invalidar a sessão inteira derrubaria também o login de cliente que estivesse aberto na mesma janela. Se a sua regra for "sair de tudo", invalide de propósito; só não faça isso por ter copiado o controller padrão.

Para onde vai quem não está logado

Sem configuração, o middleware auth manda todo visitante para a rota login, que é a do cliente. Em bootstrap/app.php, decida pelo caminho:

use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

->withMiddleware(function (Middleware $middleware) {
    $middleware->redirectGuestsTo(fn (Request $request) =>
        $request->is('equipe', 'equipe/*') ? route('equipe.login') : route('login')
    );
})

Dentro de uma rota protegida por auth:equipe, o middleware também define esse guard como o padrão da requisição. Por isso $request->user(), Auth::user() e as policies devolvem o funcionário logado, sem você repetir o nome do guard em todo lugar.

Recuperação de senha de cada porta

Use Password::broker('funcionarios') no fluxo da equipe e o broker padrão no do cliente. O e-mail de redefinição da equipe precisa apontar para a rota interna; sobrescreva no modelo Funcionario o método sendPasswordResetNotification($token) para enviar uma notificação própria com o link de /equipe. Se a equipe entra por matrícula, ainda é preciso um e-mail cadastrado para o envio, e essa mudança de campo tem efeitos que o post campo de login nem sempre é e-mail detalha.

Erros comuns

  • Uma tabela users com flag de administrador. Cada rota precisa lembrar de conferir a flag, e o esquecimento vira acesso indevido.
  • Proteger rotas internas só com auth. O guard padrão é o do cliente, e um cliente logado passa pelo middleware.
  • Compartilhar a tabela de tokens de senha. E-mails iguais nos dois cadastros fazem um token apagar o outro.
  • Copiar o logout padrão com invalidate. Sair da área interna desloga também o cliente aberto no mesmo navegador.
  • Esquecer o redirecionamento de visitantes. O funcionário que perdeu a sessão cai na tela de login de cliente e acha que a conta sumiu.
  • Mensagem de erro que diz qual campo falhou. Quem está de fora descobre quais matrículas existem.

Checklist

  • Cliente e equipe têm tabelas, modelos, providers e guards próprios.
  • Todas as rotas internas estão dentro de auth:equipe.
  • Cada guard tem seu broker de senha e sua tabela de tokens.
  • O e-mail de redefinição da equipe aponta para a área interna.
  • O logout de um guard não derruba o outro, a menos que isso seja a regra.
  • Visitantes de /equipe são enviados ao login da equipe.
  • O login regenera a sessão e a mensagem de erro é genérica.

Abra duas janelas anônimas, entre como cliente numa e como funcionário na outra, e depois faça as duas coisas na mesma janela. Sair da área interna derruba o cliente, e o link de "esqueci a senha" de cada porta chega para a conta certa?

Avalie este artigo

Referência: https://medium.com/@sayhicoelho/laravel-5-4-trabalhando-com-autenticacoes-independentes-sem-packages-adicionais-a5c16c4e8c1e