در این آموزش ، قصد داریم سیستم احراز هویت بدون پسورد فریمورک لاراول را مورد بررسی قرار دهیم. هدف اصلی این مقاله ایجاد یک راهکار حفاظتی احراز هویت سفارشی در لاراول است.

چرا احراز هویت بدون پسورد در لاراول؟

در بعضی موارد به دلایل امنیتی نمی‌خواهیم کاربران دارای رمز عبور باشند. فرض کنید بخواهیم یک لینک به آدرس ایمیل کاربر ارسال کرده و کاربر با کلیک بر روی آن لینک به حساب کاربری خودش دسترسی پیدا کند.

در این آموزش، ما در یکتادیجی فرایندی را که می‌توانید برای پیاده‌سازی این مورد استفاده کنید، توضیح می‌دهیم.

محور اصلی این فرایند ایجاد یک signed URL است که به ما امکان ارسال یک URL خاص به آدرس ایمیل کاربر را می‌دهد و فقط کاربری که این لینک را دریافت کرده است، باید به این URL دسترسی داشته باشد.

برای قدم اول باید فیلد رمز عبور را از جدول Model، Migration و Model Factory حذف کنید. از آنجایی که این فیلد به طور پیش فرض یک ستون قابل تغییر نیست، باید اطمینان حاصل کنید که آن را حذف کرده‌اید.تا اینجای کار فرآیندی نسبتاً ساده است، بنابراین هیچ مثال کدی برای این بخش نخواهیم نوشت. همزمان با این کار، می‌توانیم جدول بازنشانی رمز عبور (Password resets) را نیز حذف کنیم، زیرا ما رمز عبوری برای بازنشانی نخواهیم داشت.

قدم بعدی که باید به آن توجه کنیم، مربوط به Routing است. می‌توانیم Login route به سیستم را به عنوان یک route ساده ایجاد کنیم، برای این مثال از Livewire استفاده خواهیم کرد. بیایید نگاهی به این route بیندازیم:

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

ما می‌خواهیم این قسمت را در Middleware به صورت guest قرار دهیما اگر کاربر قبلاً وارد سیستم شده است، مجبور به تغییر مسیر شود. در این مثال از UI عبور خواهیم کرد، اما در پایان آموزش، یک لینک به مخزن GitHub وجود دارد. بیایید از طریق کامپوننت 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');
    }
}

Component که ما می‌خواهیم از آنها استفاده کنیم دارای دو ویژگی است:

1- ایمیل برای دریافت ورودی فرم استفاده می‌شود.

2- سپس status، بنابراین ما نیازی به وابستگی به درخواست session نداریم.

ما یک متد داریم که قوانین اعتبار سنجی (validation rules) را برمی‌گرداند.ترجیح ما این روش برای قوانین اعتبارسنجی (validation rules) در یک کامپوننت Livewire است. متد submit متد اصلی برای این کامپوننت است و این یک قانون نامگذاری است که ما در ارتباط با کامپوننت‌های فرم استفاده می‌کنیم. اما شما می توانید با خیال راحت نامگذاری دیگری را انتخاب و از آن استفاده کنید. ما از Container Laravel برای inject an action Class در این متد استفاده می‌کنیم تا یک URL امضا شده (signed URL) را به اشتراک بگذاریم. تنها کاری که در اینجا باید انجام دهیم این است که ایمیل وارد شده را به action منتقل کنیم و (status) را تنظیم کنیم که به کاربر هشدار دهد ایمیل در حال ارسال است.

حالا بیایید از اقداماتی که می‌خواهیم انجام دهیم، گام به گام عبور کنیم.

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 فقط نیاز به ارسال ایمیل دارد. ما می‌توانیم این کار را در صورت نیاز به صف بریزیم – اما در صورت بررسی action که نیاز به پردازش سریع دارد، بهتر است آن را در صورت ساختن یک API در صف بریزیم. ما یک class mailable به نام LoginLink داریم که از طریق URL مورد استفاده قرار می‌گیرد. URL ما با ارسال نام route که می‌خواهیم route برای آن ایجاد کنیم و پاس دادن پارامترهایی که می‌خواهید به عنوان بخشی از امضا استفاده کنید ایجاد می‌شود.

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 را ثبت کنیم و ببینیم چگونه به نظر می‌رسد.

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 از یک controller استفاده کنیم و مطمئن شویم که signed middleware را اضافه کرده‌ایم. حالا بیایید به controller نگاه کنیم تا ببینیم چگونه با آدرس‌های 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 دارای امضای معتبر است.در صورت عدم اعتبار امضا، می‌خواهیم پاسخی غیرمجاز ارسال کنیم.پس از اطمینان از اعتبار امضا، ما می‌توانیم برای کاربرهایی که passed شده query زده و آن‌ها را تأیید کنیم. در نهایت، ما یک Redirect به داشبورد برمی‌گردانیم.اکنون کاربر ما با موفقیت لاگین کرده است و مراحل ما به پایان رسیده است. با این حال، ما باید به Route ثبت نام نیز نگاه کنیم. بیایید این Route را نیز اضافه کنیم. باز هم این یک Route نمایشی خواهد بود.

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

ما دوباره به جای استفاده از session برای گرفتن name, email از متغییر status استفاده میکنیم.همچنین باز هم از یک متد rules برای بازگرداندن validation rules برای این درخواست استفاده می‌کنیم. در ادامه به متد submit برمی‌گردیم، در این بخش، می‌خواهیم دو اقدام انجام دهیم.

CreateNewUser اقدامی است که برای create و return a new user بر اساس اطلاعات دریافتی استفاده می‌کنیم. در صورتی که این فرایند به هر دلیلی شکست خورد، یک validation exception را در رابطه با آدرس ایمیل ارسال می‌کنیم. سپس از اقدام SendLoginLink که در فرم ورود استفاده شده بود، استفاده می‌کنیم تا تکرار (duplication) کد را کمتر کنیم .

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

ما می‌توانیم نام route login store را تغییر دهیم، اما به طور فنی همان کار را دوباره انجام می‌دهیم. یعنی یک کاربر جدید را ایجاد می‌کنیم و سپس می‌خواهیم آن کاربر را وارد سیستم کنیم.

یکی از روش هایی که می توان بدون رمز عبور وارد وب سایت شد را در این مقاله با هم مرور کردیم. لینک گیت هاب این پروژه را می توانید در اینجا پیدا کنید و جزئیات بیشتری در رابطه با این پروژه پیدا کنید.

دیدگاهتان را بنویسید