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

در این مطلب به صورت کامل توضیح می‌دهم که jwt چیست و چه کار می‌کند و یک مثال عملی هم در استفاده از آن در api هایی با استفاده از php می‌نویسیم خواهم زد.

برای مثال یک API خیلی ساده درست کردم که برای احراز هویت و دسترسی دیگران به این API با jwt محدودیت ایجاد می‌کنم.

jwt چیست؟

jwt مخفف Json Web Token است. یعنی توکن‌هایی که به صورت یک رشته json هستند.

قبل از اینکه جلوتر از این برویم بگذارید ببینیم اصلا توکن یعنی چه؟

توکن، چیزی مثل کارت شناسایی

توکن را کلید ورود به خانه یا کارت ملی برای انجام دادن کارهای اداری و بانکی در نظر بگیرید. برای اینکه در بانک کار شما را انجام بدهند از شما کارت ملی می‌خواهند. همین کارت هم تمام کار احراز هویت شما را انجام می‌دهد و شما دسترسی به حساب خود خواهید داشت.

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

در مثال ما قرار است یک API داشته باشیم که دسترسی به آن فقط برای دارندگان توکن مجاز است.

JWT یک توکن به صورت رشته JSON

در این بحث ما توکنی که داریم از آن حرف می‌زنیم به صورت یک رشته json است (با json که حتما آشنا هستید؟).

در این توکن اطلاعات کاربری که توکن به نامش صادر شده و یک سری چیزهای دیگر که درباره آن حرف می‌زنیم در آن وجود دارد.

JWT چه شکلی است؟

تمام jwt ها چنین ساختار سه بخشی دارند که با نقطه از همدیگر جدا می‌شوند:

Header.Payload.Signature

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLm9yZyIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUuY29tIiwiaWF0IjoxMzU2OTk5NTI0LCJuYmYiOjEzNTcwMDAwMDAsImRhdGEiOnsiaWQiOiI5IiwiZmlyc3RuYW1lIjoiTWlrZSIsImxhc3RuYW1lIjoiRGFsaXNheSIsImVtYWlsIjoibWlrZUBjb2Rlb2ZhbmluamEuY29tIn19.h_Q4gJ3epcpwdwNCNCYxtiKdXsN34W9MEjxZ7sx21Vs

JWT چطور کار می‌کند؟

سمت سرور یک توکن یا همان jwt ایجاد میشود که داخل آن اطلاعات هویتی کاربر (مثل نام کاربری یا id کاربر در دیتابیس یا هرچیزی که شما به عنوان برنامه نویس نیاز داشته باشید) به صورت کد گذاری شده قرار داده شده است.

بعد از ایجاد شدن، این توکن برای کاربر فرستاده می‌شود. کاربر به همراه ریکوئست‌هایی که به سمت سرور برای استفاده از API می فرستد، توکن jwt را هم باید بفرستد. تا سرور متوجه شود که این کاربر تایید شده است و به او اجازه دسترسی به api بدهد.

هر کدام از قسمت های JWT چه کاری انجام میدهند؟

گفتیم هر jwt سه بخش دارد ببینیم هر کدام از اینها چه هستند.

Header

header یک رشته json است که داخل آن اطلاعات مربوط به الگوریتم رمزگذاری قرار داده می‌شود.

یک مثال از header:

$header = '{
"typ":"JWT",
 "alg":"HS256"
}';

در مثال بالا دو قسمت وجود دارد:

typ: که مشحص میکند این مربوط به JWT است.

alg: که الگوریتم رمزگذاری را تعیین کرده.

Payload

این هم یک رشته جیسون است که داخل آن هر اطلاعاتی که بخواهید می‌توانید جا دهید: مثلا ID کاربر در دیتابیس یا اسم کاربر و هر اطلاعات دیگری.

فقط حواستان باشد با اینکه این رشته رمزگذاری می‌شود اما اطلاعات حساس مثل ایمیل یا رمز عبور را اینجا نگذارید. و فقط چیزهایی که برای احراز هویت ضروری است را قرار دهید.

معمولا به صورت استاندارد یک سری مقادیر وجود دارد که آنها را می‌توان به Payload اضافه کرد اما الزامی نیستند.

مثال یک Payload

$Payload = '{
"user_id":"5",
"name" : "reza",
"exp" : "2020-30-12 00:00:00"
}'

عبارت های user_id و name چیزهایی است که من برای احراز هویت کاربر نیاز دارم و exp مقداری است که تعیین کننده تاریخ انقضا استفاده از این توکن است یعنی از این تاریخ به بعد دیگر این توکن معتبر نیست.

به Payload ادعا(Claim) هم می‌گویند. چون در واقع با فرستادن توکن کاربر ادعا می‌کند هویت من این چیزی است که در playload نوشته شده. حالا باید نرم افزار این هویت را تایید یا رد کنید.

Singnature

از اسمش معلوم است یعنی امضا.

همانطور که هر شخصی امضای مخصوص به خودش را دارد امضای هر توکن jwt هم منحصر به فرد است.

اینجا هم ما باید یک رشته رمز گذاری شده درست کنیم و در توکن جا بدهیم تا فقط خودمان بتوانیم درستی یا نادرستی توکن را تشخیص بدهیم.

امضا چگونه درست میشود؟

برای درست کردن امضا باید قسمت Header و Payload را با هم ترکیب کنیم و هر کدام را با همان الگوریتمی که برای Header استفاده کردیم رمزگذاری کنیم:

base64($header) . "." . base64($Payload)

اینجا هر دو قسمت را بعد از رمز گذاری با یک نقطه کنار هم گذاشته‌ام.

و در آخر یک کلید امنیتی(Secret Key) درست می‌کنیم و با این کد این قسمت را رمز گذاری می‌کنیم.

کد امنیتی را هیچ کس نباید غیر از خودتان داشته باشد. بهتر است آن را در یک فایل config ذخیره کنید و در موقع استفاده آن را بخوانید.

با همین کار قسمت Signature ساخته می‌شود.

چطور به صورت عملی از jwt در php استفاده کنیم؟

وقتش رسیده که یک مثال عملی با jwt انجام بدهیم تا دقیق نحوه کار آن را ببینیم.

اینجا من یک API ساده درست میکنم و کاری میکنم که فقط با استفاده از jwt احراز هویت و دسترسی به API امکان پذیر باشد.

یک پوشه برای این پروژه به نام api_example درست کرده‌ام.

یک سری اطلاعات هم در دیتابیس دارم که برای دسترسی دیگران به این ها api درست می‌کنم. این اطلاعات داخل یک جدول به نام users هست که تمام کاربرهای من آنجا ثبت شده‌اند.

آموزش jwt
اطلاعات کاربران برای ساخت api

یک پوشه دیگر درون پروژه‌ام دارم به اسم api و داخل آن فایلی گذاشته ام به نام users.php درون این پوشه کدهای api من قرار دارند.

قرار است هر کسی دسترسی به api من داشت بتواند ایمیل کاربرها را ببیند. خب قطعا جون این اطلاعات خیلی حساس است پس برای دسترسی دادن باید از یک روش با امنبت بالا مثل jwt استفاده کنیم.

داخل users.php هم ایدی اسم وایمیل کاربرها را به صورت json نشان می‌دهم.

این هم کدهای فایل users.php :

<?php
//Api for Accessing users data

//connection to database
$db= new mysqli('localhost','root','','api_example');

//sql query for reading users data from database
$sql = "SELECT id,name,email FROM `users`";

//run query and sace data into variable
$result = $db->query($sql);
//get results from db 
$result = $result->fetch_all(MYSQLI_ASSOC);

//set headers for browser to understand this is json format
header("Access-Control-Allow-Origin: *");
header('Content-Type: application/json; charset=UTF-8');

//convert array to json

$users_data_json = json_encode($result,1);

//present data;
echo $users_data_json;

الان اکر این آدرس را در مرورگر وارد کنید میبینید که لیست کاربرهای من با فرمت json نشان داده می‌شود.

http://localhost/api_example/api/users.php

یک مثال از api

اما خب این API کاملا باز است وهر کسی با زدن این url به آن دسترسی دارد. من هیچ احراز هویتی انجام ندادم.

حالا می‌رسیم به بخش اصلی و مهم ساخت توکن jwt.

ساختن توکن JWT

قبل از هر چیز من در روت پروژه یک فایل index.php درست می‌کنم و در این فایل فرمی برای درخواست API می‌سازم. تا کاربری که وارد سایت شد نام کاربری خودش را وارد کند و با کلیک روی دکمه درخواست، کد مخصوص خودش را دریافت کند.

کدهای فایل index.php :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

<form action="api/jwt-create.php" method="post">
<label for="name">نام کاربری</label>
<input type="text" name="name" id="name">
<input type="submit" value="درخواست api">

</form>
</body>
</html>



فرم درخواست api

وقتی کاربر نام کاربری را وارد کرد و دکمه درخواست را زد، به فایل jwt-create.php ارسال می‌شود. آنجا این اسم را دریافت می‌کنیم و کار ساختن کد jwt را انجام می‌دهیم. پس باید در پوشه API یک فایل هم به نام jwt-create.php درست کنیم تا در این فایل کارهای ساخت توکن را انجام بدهیم.

برای ساختن jwt از یک کتابخانه معروف php به نام php-jwt استفاده می‌کنیم تا کار رمزگذاری و رمزگشایی توکن‌ها برای ما راحت‌تر شود.

من برای وارد کردن این کتابخانه در پروژه از ابزار composer استفاده می‌کنم.

کافی است این دستور را در خط فرمان IDE وارد کنید(البته کامپوزر باید روی سیستم نصب باشد).

composer require firebase/php-jwt

بعد هم فایل autoload.php را در jwt-create.php لود کنید و همینطور namespace این کتابخانه را در صفحه استفاده کنید.

این شکلی:

<?php
//here we create jwt for accessing others to our api
include '../vendor/autoload.php';

//use php-jwt namespace
use \Firebase\JWT\JWT;

یادتان هست گفتیم jwt سه بخش دارد به اسم Header، Paylod و Signature. اینجا باید این سه بخش را درست کنیم.

کتابخانه php-jwt کار ما را راحت می‌کند. فقط یک آرایه به عنوان payload و یک کلید امنیتی از ما دریافت می‌کند و کار ساخت هر سه بخش و رمز گذاری آنها را برای ما انجام می‌دهد.

قبل از هر چیز نام کاربری که کاربر ارسال کرده را اینجا می‌گیریم و در یک متغیر ذخیره می‌کنیم:

$user_name = $_POST['name'];

ساخت کلید امنیتی

در مرحله بعد باید یک secret key یا کلید امنیتی برای خودمان تعریف کنیم.

این کلید بهتر است یک رشته طولانی باشد و دست هیچ کسی غیر از خودمان نباشد.

قاعدتا این کلید نباید مستقیم در سورس کد ما جا بگیرد و بهتر است آن را در یک فایل config جدا گانه قرار بدهیم. من اینجا برای نشان دادن در همین فایل گذاشته‌ام اما این کار از لحاظ امنیتی کار درستی نیست.

$secret_key = "SECRET_KEY";

بعد باید قسمت payload را بسازیم. بقیه کارها را خود کتابخانه php-jwt برای ما انجام می‌دهد.

قبل‌تر گفتیم که Payload یک سری مقادیر دارد که می‌توانید آن ها را وارد کنید مثل تاریخ انقضا یا دامنه‌ای که کاربر مجاز به ارسال ریکوئست از آن است. علاوه بر این‌ها می‌شود مقادیری که خودتان دوست دارید را به آنها اضافه کنید.

در این مثال چون قرار است کد jwt برای هر نام کاربری متفاوت باشد من در این قسمت نام کاربری را به آن اضافه می‌کنم.

برای اضافه کردن دیتا به payload بهتر است اطلاعات را درون یک آرایه قرار دهید.

من فقط نام کاربری را گذاشته‌ام شما میتوانید id کاربر در دیتابیس یا اطلاعت دیگر را هم قرار دهید اما سعی کنید تعدادشان زیاد نشود. ضمنا متغیرهای دیگر را کامنت می‌کنم چون برای ساده سازی کار نمی‌خواهم از آنها استفاده کنم اما شما میتوانید هرکدام را خواستید قرار دهید.

$payload = array(
    // "iss" => "http://example.org",
    // "iat" => time()
    // "nbf" => time() + 10,
    // "exp" => time() + 3600
    $data = array(
        'name' => $user_name
    )
);

متغیرهای پیش فرض jwt:

iss: یک رشته که مشخص کننده ارسال کننده ریکوئست است میتواند دامنه یا نام اپلیکیشن یا هرچیزی که مشخصه فرد ارسال کننده ریکوئست است باشد.

iat: مشخص کننده زمانی است که توکن صادر شده است.

nbf: زمانی را تعیین میکند که قبل از آن استفاده از توکن امکان پذیر نیست.

exp: تاریخ انقضا توکن.

در آخر برای ساخت jwt از تابع encode خود کتابخانه php-jwt استفاده می‌کنیم و آن را درون یک متغیر می‌گذاریم.

$jwt = JWT::encode($payload, $secret_key);

پارامتر اول باید payload باشد و دومی هم کلید امنیتی که اول کار ساخته بودیم بقیه کارها مثل ساخت هدر و امضا را php-jwt برایمان انجام می‌دهد.

الان توکن ما در متغیر jwt$ قرار دارد. اینجا می‌توانیم به کاربر نمایش دهیم:

echo "توکن شما با موفقیت ساخته شد" . "</br>";
echo "<pre><samp>" . $jwt . "</samp></pre>";

تمام کدهای فایل jwt-create.php :

<?php
//here we create jwt for accessign others to our api
include '../vendor/autoload.php';

use \Firebase\JWT\JWT;

$user_name = $_POST['name'];

//set a powerful encoded secret key
// $secret_key = hash_hmac('ripemd160', 'secret key must not be available for everyone', 'secret');
$secret_key = "rezasalam";

//se data for putting in token
$payload = array(
     // "iss" => "http://example.org",
    // "aud" => "http://example.com",
    // "iat" => 1356999524,
    // "nbf" => 1357000000,
    $data = array(
    'name' => $user_name
    )
);

//create jwt 
$jwt = JWT::encode($payload, $secret_key,'HS256');

//print the created token for user
echo "توکن شما با موفقیت ساخته شد" . "</br>";

echo "<pre><code>" . $jwt . "</code></pre></br>";
echo $secret_key;

با انجام این مرحله دیگر عملیات ساخت توکن برای کاربرها به درستی انجام می‌شود.

واضح و منطقی است که در یک برنامه واقعی من برای هر کسی توکن صادر نمی‌کنم. پس باید قبل از صدور توکن چک کنم که این نام کاربری مورد تایید من هست یا خیر. من برای اینکه مثال ساده باشد و فقط اصل کار را بیان کنم از نوشتن موارد این چنینی صرف نظر کرده‌ام.

الان باید کاری کنیم قبل از ارائه API به کاربر صحت توکن jwt چک شود.

اعتبار سنجی توکن JWT قبل از ارائه API به کاربر

وارد فایل users.php می‌شویم. و در این فایل قبل از کدهایی که برای api نوشته بودیم باید کار چک کردن صحت توکن را انجام بدهیم.

قبل از هر چیز فایل autoload.php را اینجا هم include می‌کنیم و namespace کتابخانه php-jwt را وارد می‌کنیم. تا بتوانیم برای رمزگشایی توکن از این کتابخانه استفاده کنیم.

include '../vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

اول چک می‌کنیم که آیا کاربری که به این صفحه ریکوئست داده، توکن خودش را وارد کرده و اگر وارد کرده، توکن او صحیح است یا خیر.

پس دیتایی که کاربر به همراه ریکوئست فرستاده را دریافت می‌کنیم.

این شکلی:

$data = json_decode(file_get_contents("php://input"));

file_get_contents("php://input") هر دیتایی که کاربر با هر متدی فرستاده است را دریافت می‌کند.

چون دیتا به صورت جیسون فرستاده می‌شود اینجا با تابع json_decode آن را به آرایه تبدیل می‌کنیم ودر data$ می‌گذاریم.

کلید امنیتی را هم که نوشته بودیم دوباره می‌آوریم.

$secret_key = "SECRET_KEY";

نکته: باید در داکیومنت API خودتان بنویسید که استاندارد دریافت این کد چطور است و کاربر درخواست کننده در چه کلیدی این کد را بفرستد من اینجا فرض می‌کنم که کاربر آن را با فرمت JSON و در کلید به نام jwt می فرستد.

متد ارسال ریکوئست هم اینجا باید post باشد چون قرار است یک توکن امنیتی فرستاده شود فرستادن به صورت GET امن نیست.

پس الان باید چک کنیم که jwt وجود دارد یا نه و بعد آن را رمزگشایی کنیم و چک کنیم که اطلاعات داخل آن صحیح هست یا خیر.

توکنی که از ریکوئست گرفتیم را در متغیری به نام $jwt می‌گذاریم.

$jwt=isset($data->jwt) ? $data->jwt : "";

کد بالا چک میکند که در دیتای ارسال شده کلید jwt وجود دارد یا خیر اگر وجود داشت خودش را می‌گذارد و اگر نبود یک رشته خالی برمیگرداند.

در مرحله بعد چک می‌کنیم که اگر مقدار jwt موجود بود کد توکن را رمزگشایی کند با متد decoded:

if($jwt){

    try{
        $decoded = JWT::decode($jwt,$secret_key, new Key($key, 'HS256'));
        http_response_code(200);
        
    }
    catch(Exception $e){
        http_response_code(401);
 
        // show error message
        echo json_encode(array(
            "message" => "Access denied.",
            "error" => $e->getMessage()
        ));
    }



    
    
}else{
    header('HTTP/1.0 401 Unauthorized');
    echo json_encode(array(
        "message" => "شما دسترسی به این صفحه ندارید"
    ));

    die();
}

کد بالا اول خالی نبودن متغیر jwt را چک می‌کند اگر خالی بود یک هدر 401 و پیغام خطا برمیگرداند.

اگر خالی نبود jwt را رمزگشایی می‌کند

برای رمزگشایی از متد JWT::decode استفاده میشود که سه پارامتر میگیرد کد توکن که از کاربر دریافت کردیم، secret key و پارامتر سوم هم الگوریتم رمزگذاری که با توجه به داکیومنت کتابخانه jwt همین array('HS256') را برای آن می‌نویسیم.

اگر رمز گشایی موفقیت آمیز باشد کد ادامه پیدا میکند و ما با http_response_code(200) به مرورگر می‌فهمانیم که همه چیز درست بوده است. دیگر کد وارد else نمی‌شود و در ادامه API به کاربر نمایش داده می‌شود.

اما اگر رمزگشایی با موفقیت انجام نشود خود کتابخانه php-jwt یک خطا برمی‌گرداند که اینجا در کد با استفاده از try catch خطا گرفته می‌شود و به کاربر نمایش داده می‌شود.

خب تا این مرحله هم قسمت ساخت توکن درست شد و هم بررسی توکن برای دسترسی دادن به api.

فقط می‌ماند تست کردن هر دو اینها.

اول برویم و به عنوان کاربر این سایت یک توکن دریافت کنیم.

وارد صفحه اول سایت می‌شویم

localhost/api-example

چون فایل index.php وجود دارد مرورگر مستقیم وارد این صفحه می‌شود تا فرم درخواست api را ببنیم.

من یک نام کاربری اینجا وارد می‌کنم.

توکن jwt
فرم درخواست api برای کاربران

با زدن دکمه درخواست به صفحه‌ای منتقل می‌شویم و کد توکن jwt به ما نمایش داده می‌شود.

توکن ساخته شده برای کاربر

الان برویم امتحان کنیم با این توکن می‌توانیم به api دسترسی داشته باشیم یا نه.

برای تست با استفاده از توکن اینجا باید از نرم‌افزار postman استفاده کنیم. postman نرم افزاری است که کار ارسال ریکوئست را برای ما می‌تواند شبیه سازی کند.

نکته: برای ریکوئست دادن به api از طریق php باید از curl یا کتابخانه های مرتبط مثل guzzlehttp استفاده کرد اما اینجا چون فقط می‌خواهیم درست کار کردن jwt را امتحان کنیم نیازی به ساختن یک صفحه php و کار با curl نیست.همین postman جواب کار ما را خواهد داد. اما اگر کسی بخواهد از سایت یا نرم افزار خودش برای api ما ریکوئست بفرستد باید از curl استفاده کند.

وارد postman می‌شویم. یک قسمت آدرس دارد که آدرس صفحه users.php را وارد میکنیم و متد را هم باید POST بگذاریم چون قرار است توکن jwt را به همراه ریکوئست بفرستیم.

بعد باید به همراه ریکوئست توکن را هم قرار دهیم برای این کار از قسمت Body گزینه raw را انتخاب میکنیم و نوع داده را هم JSON می‌گذاریم.

در کادر پایین به صورت json کد توکن را که قبل تر دریافت کرده بودیم با کلید jwt قرار می‌دهیم. این شکلی:

در آخر روی Send کلیک میکنیم تا ریکوئست ارسال شود.

نتیجه ریکوئست api

مشاهده کردید که چون توکنی که دریافت کرده بودیم را دقیق و درست ارسال کردیم احراز هویت انجام شد و نتیجه نشان داده شد.

اگر دقت کرده باشید در فرایند چک کردن jwt هیچ کجا من هیچ دیتایی را از دیتابیس نخواندم که بخواهم با آن چک کنم چون خود jwt هر چه نیاز دارم را در خودش قرار داده. به خاطر همین اصطلاحا میگویند jwt خودشمول یا self contained است.

موقع ساخت توکن نام کاربری را در payload قرار دادم چون نام کاربری منحصر به فرد است پس توکن ساخته شده هم با بقیه توکن‌ها متفاوت خواهد بود.

هیچ کس دیگری هم نمی‌تواند یک توکن مثل این را بسازد حتی اگر نام کاربری را داشته باشد چون کلید امنیتی من را ندارد باز هم این کار غیر ممکن است. برای همین secret key خیلی مهم است و نباید هیچ کس از آن مطلع شود.

جمع بندی

jwt یکی از امن ترین روش ها برای دسترسی دادن به api هاست. برای همین در مواردی که نیاز به محدود کردن دسترسی و احراز هویت دقیق در API داریم از این روش استفاده میکنیم.

کتابخانه php-jwt تمام کارهای رمزگذاری و رمزگشایی را برای ساخت توکن jwt انجام می‌دهد فقط ما باید قسمت payload را به آن بدهیم.

کوچکترین تغییر داخل هر کدام از بخش های این توکن کل توکن را بی‌اعتبار می‌کند.

سعی کردم تمام چیزی که یادگرفته بودم و خودم پیاده سازی کردم را اینجا به اشتراک بگذارم.

البته jwt کاربردهای دیگری مثل احراز هویت کاربران برای ثبت نام در سایت هم دارد اما بیشتر در api ها استفاده می‌شود.