Laravel 无密码授权登录
有些情况下,我们不希望用户通过密码访问。有时,我们希望通过发送魔术链接到用户邮箱,让他们点击链接授权访问。
本教程中,我将向你介绍一个可以自己实现的处理过程。这一工作流的重点是,创建一个签名 URL,让我们可以发送特定的 URL 到用户邮箱中,使得只有该用户能够访问此 URL。
我们需要先从 migration、model 和模型工厂中删除 password 字段。由于不需要该字段,我们需要确保将其移除,因为该字段默认不是 nullable
字段。此处理过程很简单,此处不予展示。同时,我们也可以删除密码重置表,因为密码字段没了,也就无所谓重置了。
接下来,我们该关注的是路由。我们创建一个简单的视图路由作为登录路由,本例中,我们将使用 Livewire 构建。路由如下:
Route::middleware(['guest'])->group(static function (): void {
Route::view('login', 'app.auth.login')->name('login');
});
此处使用 guest 中间件,如果用户已登录则强制重定向。本例就不展示 UI 部分了。我们一起看看用于登录表单的 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');
}
}
该组件有两个属性。Email 用于获取表单输入;然后是 status
,借由该字段我们无需依赖于请求 session。另外还有一个方法返回验证规则。我个人偏好于在 Livewire 组件定义验证规则。submit
方法是该组件的重点,这是我处理表单组件时的命名规范。我们使用 Laravel 容器将一个 action
类注入到该容器,使之共享创建和发送签名URL的逻辑。此处我们要做的就是,将用户输入的邮箱传给 action
,并设置 status 提醒邮件已发送。
现在,我们来看看我们要用的 action
类。
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,
),
)
);
}
}
该 action 只负责发送邮件。如果需要我们可以将其配置成队列 - 不过,如果我们在创建 API,当我们使用的 action 需要快速处理的话,最好还是使用队列。我们有一个 mailable
的类,叫做 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 [];
}
}
该 mailable
类相对简单,和标准的 mailable
相比没有太大延迟。我们为该 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');
});
我们为该路由分配一个控制器,并确保添加了 signed
中间件。接下来,我们看看如何在控制器中处理签名 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 是有效的签名,如果不是,则抛出未授权响应。如果签名有效,我们可以查询用户并进行授权。最后,我们返回重定向到 dashboard。
现在用户已经成功登录,我们的旅程也完成了。不过,我们还需要再查看一下注册路由。再次添加该路由,此路由同样是视图路由。
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');
}
}
我们获取用户名,邮箱地址,并且还是使用 status 属性替代 request session。然后我们还是用 rule 方法来返回请求的验证规则。最后是 submit
方法,这一次我们注入了两个 action。
CreateNewUser
是我们基于用户输入的信息来创建并返回新用户的 action。如果此处失败,我们抛出验证异常。然后我们复用 SendLoginLink
来减少代码重复。
final class CreateNewUser
{
public function handle(string $name, string $email): Builder|Model
{
return User::query()->create([
'name' => $name,
'email' => $email,
]);
}
}
这是无密码授权的一种实现方法。