在Laravel项目中的实现无密码认证之:发送邮箱链接授权

2023-06-01 00:00:00 发送 授权 邮箱

laravel项目中无密码认证需求:

有时候,我们不希望用户有密码。

有时,我们想发送一个神奇的链接到用户的电子邮件地址,让他们点击以获得访问权。


解决方式:

定向邮箱发送授权链接

介绍一个你可以用来自己实现的流程,这个工作流程的重点是创建一个签名的URL,

让我们向用户的电子邮件地址发送一个特定的URL,而且只有这个人能够访问这个URL。


实现思路:

我们首先要从我们的迁移、模型和模型工厂中删除密码字段。

由于不需要,我们要确保删除它,因为它在默认情况下不是一个可忽略的列。

这是一个相对简单的实现过程,所以我不会为这部分展示任何代码示例。

同时,我们可以删除密码重置表,因为我们将没有密码需要重置。


路由应该是我们接下来要看的东西。

我们可以把登录路由创建为一个简单的视图路由,因为我们将在这个例子中使用Livewire。

让我们来看看如何注册这个路由:

Route::middleware(['guest'])->group(static function (): void {
    Route::view('login', 'app.auth.login')->name('login');
});

我们想把这个包在访客中间件中,以便在用户已经登录的情况下强制重定向。

我不会去看这个例子的用户界面,但在教程的最后,有一个指向GitHub上的repo的链接。

让我们来看看我们将用于登录表单的Livewire组件。

final class LoginForm extends Component
{
    public string $email = '';
 
    public string $status = '';
 
    public function submit(SendLoginLink $action): void
    {
        $this->validate();
 
        $action->handle(
            email: $this->email,
        );
 
        $this->status = 'An email has been sent for you to log in.';
    }
 
    public function rules(): array
    {
        return [
            'email' => [
                'required',
                'email',
                Rule::exists(
                    table: 'users',
                    column: 'email',
                ),
            ]
        ];
    }
 
    public function render(): View
    {
        return view('livewire.auth.login-form');
    }
}

我们的组件有两个我们要使用的属性。

电子邮件是用来捕获表单输入的。

然后是状态,所以我们不需要依赖请求会话。

我们有一个方法可以返回验证规则。

这是我对Livewire组件中验证规则的首选方法。

我们的提交方法是这个组件的主要方法,这是我在处理表单组件时使用的一个命名惯例。

这对我来说很有意义,但请自由选择适合你的命名方法。

我们使用Laravels容器将一个动作类注入这个方法,以分享创建和发送签名URL的逻辑。

我们在这里需要做的就是把输入的电子邮件传递给动作,并设置一个状态,提醒用户正在发送电子邮件。


现在让我们来看看我们要使用的动作:

final class SendLoginLink
{
    public function handle(string $email): void
    {
        Mail::to(
            users: $email,
        )->send(
            mailable: new LoginLink(
                url: URL::temporarySignedRoute(
                    name: 'login:store',
                    parameters: [
                        'email' => $email,
                    ],
                    expiration: 3600,
                ),
            )
        );
    }
}

这个动作只需要发送一封电子邮件。

如果我们想的话,我们可以将其配置为队列 - 但在处理需要快速处理的动作时,如果我们正在建立一个API,最好是排队。

我们有一个名为LoginLink的可发送类,我们通过它来传递我们想要使用的URL。

我们的URL是通过传入我们想要生成的路由的名称和传入你想要作为签名的一部分的参数来创建的:

final class LoginLink extends Mailable
{
    use Queueable, SerializesModels;
 
    public function __construct(
        public readonly string $url,
    ) {}
 
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Your Magic Link is here!',
        );
    }
 
    public function content(): Content
    {
        return new Content(
            markdown: 'emails.auth.login-link',
            with: [
                'url' => $this->url,
            ],
        );
    }
 
    public function attachments(): array
    {
        return [];
    }
}

我们的可邮寄类是相对直接的,与标准的可邮寄类没有什么区别。

我们传入一个字符串作为URL。

然后,我们想把它传递给内容中的一个markdown视图:

<x-mail::message>
# Login Link
 
Use the link below to log into the {{ config('app.name') }} application.
 
<x-mail::button :url="$url">
Login
</x-mail::button>
 
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>

用户将收到这封邮件并点击链接,将他们带到已签署的URL。

让我们注册这个路由,看看它看起来如何:

Route::middleware(['guest'])->group(static function (): void {
    Route::view('login', 'app.auth.login')->name('login');
    Route::get(
        'login/{email}',
        LoginController::class,
    )->middleware('signed')->name('login:store');
});

我们要为这个路由使用一个控制器,并确保我们添加签名的中间件。

现在让我们看看控制器,看看我们如何处理签名的URL:

final class LoginController
{
    public function __invoke(Request $request, string $email): RedirectResponse
    {
        if (! $request->hasValidSignature()) {
            abort(Response::HTTP_UNAUTHORIZED);
        }
 
        /**
         * @var User $user
         */
        $user = User::query()->where('email', $email)->firstOrFail();
 
        Auth::login($user);
 
        return new RedirectResponse(
            url: route('dashboard:show'),
        );
    }
}

我们的第一步是确保URL有一个有效的签名,如果它没有,我们要抛出一个未经授权的响应。

一旦我们知道签名是有效的,我们就可以查询通过的用户并对他们进行认证。

最后,我们返回一个重定向到仪表板。


我们的用户现在已经成功登录,我们的旅程已经完成。

然而,我们也需要看一下注册路线。

让我们接下来添加这个路由。

同样,这将是一个视图路由:

Route::middleware(['guest'])->group(static function (): void {
    Route::view('login', 'app.auth.login')->name('login');
    Route::get(
        'login/{email}',
        LoginController::class,
    )->middleware('signed')->name('login:store');
 
    Route::view('register', 'app.auth.register')->name('register');
});

同样,我们在注册表格中使用了一个Livewire组件--就像我们在登录过程中做的那样:

final class RegisterForm extends Component
{
    public string $name = '';
 
    public string $email = '';
 
    public string $status = '';
 
    public function submit(CreateNewUser $user, SendLoginLink $action): void
    {
        $this->validate();
 
        $user = $user->handle(
            name: $this->name,
            email: $this->email,
        );
 
        if (! $user) {
            throw ValidationException::withMessages(
                messages: [
                    'email' => 'Something went wrong, please try again later.',
                ],
            );
        }
 
        $action->handle(
            email: $this->email,
        );
 
        $this->status = 'An email has been sent for you to log in.';
    }
 
    public function rules(): array
    {
        return [
            'name' => [
                'required',
                'string',
                'min:2',
                'max:55',
            ],
            'email' => [
                'required',
                'email',
            ]
        ];
    }
 
    public function render(): View
    {
        return view('livewire.auth.register-form');
    }
}

我们捕获用户的名字,电子邮件地址,并有一个状态属性,而不是再次使用请求会话。

我们再次使用一个规则方法来返回这个请求的验证规则。

我们回到提交方法,这一次,我们要注入两个动作。


CreateNewUser是我们用来创建并返回一个基于所提供信息的新用户的动作。

如果这个动作因为某些原因而失败,我们会在邮件中抛出一个验证异常。

然后,我们使用我们在登录表单上使用的SendLoginLink动作,以减少代码重复:

final class CreateNewUser
{
    public function handle(string $name, string $email): Builder|Model
    {
        return User::query()->create([
            'name' => $name,
            'email' => $email,
        ]);
    }
}


以上就是实现无密码认证的许多方法之一,这确实是一种有效及便捷的方法。

相关文章