ایجاد یک JSON Web Token ساده برای APIهای لاراول
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 8 دقیقه

ایجاد یک JSON Web Token ساده برای APIهای لاراول

لاراول به تازگی برخی از ویژگی‌های کامپوزر را تغییر داده است، بنابراین این فریمورک با یک پکیج JSON Web Token که به صورت پیش‌فرض در آن قرار دارد همراه شده است. از آنجا که ما پکیج مورد نیاز برای استفاده از JSON Web Token (به اختصار JWT) داریم با ما همراه باشید تا با استفاده از JWT یک گارد احراز هویت سفارشی برای API‌ها بسازیم.

ایجاد یک JSON Web Token ساده برای APIهای لاراول

آشنایی اولیه

 اگر شما با مفهوم JWT آشنایی ندارید، شما می‌توانید با استفاده از مقاله ۵ قدم ساده برای درک JWT و یا مقاله JWT در مقابل sessionها با این مفهوم آشنا شوید. اکیداً پیشنهاد می‌شود که با این مفهوم آشنا شده و آن را بررسی کنید زیرا JWTها اغلب در اپلیکیشن‌های وب استفاده می‌شوند. در این مقاله ما به طور کلی در مورد JWTهای صحبت نمی‌کنیم و همچنین به بررسی ساختار آن‌ها نخواهیم پرداخت. ما فرض می‌کنیم که شما از قبل با این مفهوم آشنایی دارید، بنابراین با خیال راحت بر روی گارد سفارشی احراز هویت تمرکز می‌کنیم و به توضیح آن در لاراول می‌پردازیم.

روند احراز هویت سفارشی

 قبل از شروع به کدنویسی، بگذارید جریان تایید اعتبارمان را روشن کنیم. ما معمولاً از اپلیکیشن‌های ترکیبی استفاده می‌کنیم. این بدین معناست که ما صفحات استاندارد با درخواست‌های HTTP از نوع بارگیری مجدد(reload) را برای حرکت بین صفحات مختلف در اپلیکیشن خود داریم و همچنین ما از درخواست‌های AJAX برای انجام درخواست‌های HTTP در پشت صحنه استفاده می‌کنیم. در ادامه توجه کنید که در یک اپلیکیشن، کاربر لاگین می‌کند و پس از آن ما یک توکن با کمک میان افزارها(middleware) تولید می‌کنیم و سپس توکن ایجاد شده را در session ذخیره می‌کنیم و بعد آن را در یک متاتگ چاپ می‌کنیم. اگر شما می‌خواهید از اپلیکیشنی که 100درصد SPA است استفاده کنید رویکرد متفاوت خواهد بود. شما یک درخواست به صورت ایجکس برای گرفتن توکن ارسال می‌کنید سپس این توکن را در local storage ذخیره می‌کنید.

در حال حاضر این روش یک روش خیلی خوب برای احراز هویت است.

لازم به ذکر است برای هر درخواست ایجکس، ما JWT خود را به عنوان یک بیرر توکن(bearer token) در هدرها ارسال می‌کنیم و همچنین در پشت صحنه گارد JWTما کاربر مورد نظر را احراز هویت می‌کند. اگر احراز هویت موفق باشد کاربر می‌تواند به ادامه فعالیت خود بپردازد در غیر این صورت گارد ادامه‌ی درخواست‌ها را متوقف می‌کند.

Middleware تولید کننده توکن

اول از همه بیایید به توضیح چگونگی تولید JWT با کمک میان افزار بپردازیم. ما می‌توانیم با استفاده از دستور php artisan make:middleware در ترمینال یک میان افزار جدید ایجاد کنیم و نام این میان افزار را GenereateJwt می‌گذاریم:

<?php

namespace App\Http\Middleware;

use Closure;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Signer\Hmac\Sha256;

class GenerateJwt
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  int|null  $minutes
     * @return mixed
     */
    public function handle($request, Closure $next, $minutes = null)
    {
        if (! $request->user() && session()->has('token')) {
            session()->forget('token');
        } elseif ($request->user() && (! session('token') || ! $this->validate($request, session('token')))) {
            session()->put('token', $this->issue($request, $minutes));
        }

        return $next($request);
    }

    /**
     * Issue the token.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int|null  $minutes
     * @return string
     */
    protected function issue($request, $minutes)
    {
        $builder = (new Builder)
            ->setId(str_random())
            ->setIssuer($request->getHost())
            ->setAudience($request->getHost())
            ->setSubject($request->user()->id);

        if ($minutes) {
            $builder->setExpiration(time() + ($minutes * 60));
        }

        return (string) $builder->sign(new Sha256, config('app.key'))->getToken();
    }

    /**
     * Validate the token.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $token
     * @return bool
     */
    protected function validate($request, $token)
    {
        $token = (new Parser)->parse($token);

        $data = new ValidationData;
        $data->setIssuer($request->getHost());
        $data->setAudience($request->getHost());
        $data->setSubject($request->user()->id);

        return $token->validate($data);
    }
}

توجه کنید که این فقط یک تنظیم اولیه برای JWT است؛ اگر شما می‌خواهید که تنظیمات دیگری را بر روی JWTخود قرار دهید می‌توانید این مستندات را بررسی کنید و موارد مورد نیاز خود را تهیه کنید.

 دقیقاً چه اتفاقی اینجا می‌افتد؟ اول از همه ما توکن را تولید می‌کنیم در صورتی که کاربر احراز هویت شده اما هیچ توکنی در session آن وجود ندارد؛ بدین معناست که کاربر فقط وارد سیستم شده است. ما از متد issue() برای ایجاد توکن بر اساس درخواست فعلی استفاده می‌کنیم، همچنین میان افزار اجازه می‌دهد مدت زمان انقضا توکن را برای هر چند مدتی که می‌خواهیم تنظیم کنیم و همچنین اگر توکن معتبر نیست- ما این مورد را در متد validate() تشخیص می‌دهیم - احتمالاً به دلیل انقضاء توکن، توکن جدیدی ایجاد و در session ذخیره می‌شود.

اگر کاربری وجود نداشته باشد اما توکن در session تنظیم شده باشد-بدین معناست که کاربر فقط لاگین کرده است- ما توکن را از session حذف می‌کنیم. ما می توانیم از session در اینجا استفاده کنیم زیرا هنوز از session به عنوان درایور احراز هویت رابط وب استفاده می‌کنیم. در صورت انقضاء توکن، می‌توانیم توکن جدیدی ایجاد کنیم در حالی که کاربر در زمان فعال بودن توکن در session احراز هویت شده است.

 ما می‌توانیم میان افزار خود را در آرایه $routeMiddleware ریجستر کنیم:

...
'jwt.generate' => \App\Http\Middleware\GenerateJwt::class,
...

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

...

'jwt.generate',

// OR

'jwt.generate:30',
...

نکته: شما می‌توانید زمان انقضا را هر چند دقیقه‌ای که می‌خواهید تنظیم کنیم یا در صورتی که نمی‌خواهید توکن منقضی شود می‌توانید پارامتر آن را قرار ندهید، مانند بالا.

چگونگی قرار دادن و استقاده از توکن

تا اینجای کار ما توکن را داریم اما باید آن را کجا قرار دهیم؟ همچنین چطور می‌توانیم از آن در درخواست‌های ایجکس استفاده کنیم؟ ما می‌توانیم قطعه کد زیر را در تگ <header> برای چاپ توکن قرار دهیم:

@auth
    <meta name="api-token" content="{{ session('token') }}">
@endauth

همانطور که می‌بینید اگر کاربر احراز هویت شده باشد ما متاتگ حاوی توکن تولید شده را قرار می‌دهیم، همچنین می‌توانیم از آن در جاوا اسکریپت نیز استفاده کنیم. برای این کار و مدیریت درخواست‌های ایجکس ما از کتابخانه axios استفاده می‌کنیم:

if (document.head.querySelector('meta[name="api-token"]')) {
    axios.defaults.headers.common['Authorization'] = 'Bearer ' +  document.head.querySelector('meta[name="api-token"]').content;
}

با این روش ما هدر Authorization را برای axios تنظیم می‌کنیم. که شامل JWT به عنوان یک bearer token است.

گارد JWT) JWT Guard)

به صورت پیش‌فرض، لاراول یک API token guard استاتیک ارائه می‌دهد، بنابراین می‌توانیم از آن به عنوان نقطه شروع کار خود استفاده کنیم بخصوص که ما یک روش بسیار مشابه با آن داریم. در ابتدا بیاید یک کلاس JwtGuard در پوشه app/Auth ایجاد کنیم، بر اساس TokenGuard داخلی ما می‌توانیم یک  JSON Web Tokenسبک بنویسیم.

ابتدا به کد زیر نگاهی بیندازید سپس توضیح آن را مطالعه کنید:

<?php

namespace App\Auth;

use Lcobucci\JWT\Parser;
use Illuminate\Http\Request;
use InvalidArgumentException;
use Lcobucci\JWT\ValidationData;
use Illuminate\Auth\GuardHelpers;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;

class JwtGuard implements Guard
{
    use GuardHelpers;

    /**
     * The request instance.
     *
     * @var \Illuminate\Http\Request
     */
    protected $request;

    /**
     * The name of the query string item from the request containing the API token.
     *
     * @var string
     */
    protected $key;

    /**
     * Create a new authentication guard.
     *
     * @param  \Illuminate\Contracts\Auth\UserProvider  $provider
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $key
     * @return void
     */
    public function __construct(UserProvider $provider, Request $request, $key = 'api_token')
    {
        $this->key = $key;
        $this->request = $request;
        $this->provider = $provider;
    }

    /**
     * Get the currently authenticated user.
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function user()
    {
        if (! is_null($this->user)) {
            return $this->user;
        }

        try {
            $token = (new Parser)->parse($this->getTokenForRequest());

            $data = new ValidationData;
            $data->setIssuer($token->getClaim('iss'));
            $data->setAudience($token->getClaim('aud'));
            $data->setSubject($token->getClaim('sub'));

            if (! $token->verify(new Sha256, config('app.key')) || ! $token->validate($data)) {
                return;
            }

            return $this->user = $this->provider->retrieveById($token->getClaim('sub'));
        } catch (InvalidArgumentException $exception) {
            return;
        }
    }

    /**
     * Get the token for the current request.
     *
     * @return string
     */
    public function getTokenForRequest()
    {
        $token = $this->request->query($this->key);

        if (empty($token)) {
            $token = $this->request->input($this->key);
        }

        if (empty($token)) {
            $token = $this->request->bearerToken();
        }

        if (empty($token)) {
            $token = $this->request->getPassword();
        }

        return $token;
    }

    /**
     * Validate a user's credentials.
     *
     * @param  array  $credentials
     * @return bool
     */
    public function validate(array $credentials = [])
    {
        if (empty($credentials['id'])) {
            return false;
        }

        if ($this->provider->retrieveById($credentials['id'])) {
            return true;
        }

        return false;
    }
}

اول از همه، همانطور که مشاهده می‌کنید ما از قرارداد Illuminate\Contracts\Auth\Guard استفاده می‌کنیم و همچنین از تریت Illuminate\Auth\GuardHelpers کمک می‌گیریم. Key در داخل متد user() است. ما فقط متد getTokenForRequest() را از کلاس پیش‌فرض TokenGuard کپی کردیم و در اینجا نیز از آن استفاده می‌کنیم تا JWT  ارسال شده با درخواست را استخراج کنیم.

بنابراین دقیقاً چه اتفاقی می‌افتد؟ اگر قالب توکن قابل قبول برای Parser باشد. توکن پارسر می‌شود و یک توکن نمونه را باز می‌گرداند و سپس با کمک کلاس ValidationData ما می‌توانیم داده‌هایی را که می‌خواهیم اعتبار دهیم ایجاد کنید. اول از همه، ما توکن را تایید(verify) می‌کنیم، بدین معنا که ما بررسی می‌کنیم که آیا signature معتبر است، سپس توکن را اعتبارسنجی(validate) می‌کنیم که این بدین معناست که ما بررسی می‌کنیم که آیا توکن منقضی شده است یا خیر و یا آیا از قبل مورد استفاده قرار گرفته است.

اگر همه چیز خوب و درست بود ما مقدار subject claim از توکن -که حاوی Id کاربر است- را استخراج می‌کنیم و آن را به عنوان UserProvider قرار می‌دهیم. اگر با یک نمونه از کاربر بازگردد، تایید اعتبار موفقیت‌آمیز بوده است.

 ریجستر کردن گارد

لاراول یک راه خوب برای گسترش(extend) لایه احراز هویت فراهم کرده است. شما می‌توانید در مستندات لاراول مطالب بیشتری در مورد اضافه کردن گارد به متد boot از AuthServiceProvider پیدا کنیم:

// Register the JWT Guard
Auth::extend('jwt', function ($app, $name, array $config) {
    return new JwtGuard(Auth::createUserProvider($config['provider']), $app['request']);
});

همچنین ما نیاز داریم که بعضی تنظیمات در فایل auth.php را ویرایش کنیم. در این فایل ما نیاز داریم که تنظیمات API گارد را تغییر دهیم، بنابراین درایور توکن را بر روی JWT قرار می‌دهیم.

'guards' => [
    ...

    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

از اینجا به بعد، هر وقت شما از میان افزار auth:api استفاده می‌کنید از گارد JwtGuard به جای TokenGuard پیش‌فرض استفاده می‌شود.

نتیجه گیری

در این مقاله فقط یک نمایه اصلی و اولیه از کار با JWTها در لاراول ارائه شد. البته موارد زیادی وجود دارد که ممکن است در نظر داشته باشید که برای پروژه خود از آن‌ها استفاده کنید یا تغییراتی در آن‌ها ایجاد کنید.

اگر شما از قبل با JWTها آشنایی ندارید ما اکیداً توصیه می‌کنیم که این موضوع را بررسی کنید؛ زیرا JWT یک روش خیلی خوب برای تایید اعتبار درخواست‌ها در وب و API است.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
1.5 از 2 رای

2 سال پیش
/@rezajamalzadeh901

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید

ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

رضا جمال زاده