در آموزش گذشته دیدیم که به چه شکل میتوان به عنوان یک کاربر جدید ثبتنام نمود؛ حال در ادامهٔ توسعهٔ این وب سرویس قصد داریم تا قابلیت لاگین کردن به سیستم و دریافت یک توکن از جنس JWT را به پروژه بیفزاییم. برای این منظور، داخل کلاس UserController
متد جدیدی تحت عنوان ()auth
به صورت زیر میسازیم:
public function auth()
{
if ($this->requestMethod === 'POST') {
$this->validateRequest();
$this->validateParams('mail', $this->requestParams['mail'], true);
$this->validateParams('pass', $this->requestParams['pass'], true);
$user = new User((new Database())->connect());
$result = $user->login($this->requestParams['mail'], $this->requestParams['pass']);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if (!$result) {
$this->returnResponse(401, 'Email or password is incorrect.');
} else {
$this->generateToken($result[0]['id']);
}
} else {
$this->throwError(405, 'The only method that is acceptable is POST.');
}
}
همچون متد ()signup
که در آموزش گذشته مورد بررسی قرار دادیم، داخل متد ()auth
نیز ابتدا به ساکن چک کردهایم ببینیم که آیا ریکوئست ارسالی از جنس POST
است یا خیر که اگر این گونه بود، داخل بلوک if
با فراخوانی متد ()validateRequest
پارامترهای ارسالی از سمت کلاینت را سِت کرده سپس با استفاده از متد ()validateParams
از وجود کلیدهای mail
و pass
اطمینان حاصل کردهایم.
همانطور که مشاهده میشود، داخل این متد آبجکتی از روی مُدل User
ساختهایم سپس متد ()login
را روی آن فراخوانی کردهایم و به عنوان پارامترهای ورودی این متد نیز کلیدهای mail
و pass
که پیش از این داخل پراپرتی requestParams$
ذخیره شدهاند را در نظر گرفتهایم و نتیجه را در متغیری تحت عنوان result$
ذخیره کردهایم. در ادامه، با استفاده از یک دستور شرطی تست کردهایم ببینیم که آیا این متغیر حاوی مقداری است یا خیر که اگر پاسخ به این شرط false
بود، ارور 401 با پیامی مبنی بر اینکه «ایمیل یا پسورد اشتباه است.» در معرض دید کاربر قرار میدهیم و در غیر این صورت وارد بلوک else
شده و متدی تحت عنوان ()generateToken
را فراخوانی نمودهایم که در حال حاضر ساخته نشده است. به طور کلی، این متد وظیفه دارد تا پس از کوئری زدن به جدول users
و تطابق ایمیل و پسورد ارسالی از سمت کلاینت با آنچه قبلاً در دیتابیس ثبت شده است، یک توکن از جنس JWT ایجاد نماید به طوری که برای ارسال ادامهٔ ریکوئستها میتوان با ارسال آن توکن در بخش هِدِر، این تضمین را به وب سرویس بدهیم که کلاینتِ معتبری هستیم.
برای ساخت متد ()generateToken
، کلاس BaseController
را به صورت زیر تکمیل میکنیم:
<?php
namespace Core;
use Firebase\JWT\JWT;
class BaseController
{
public $requestMethod;
public $request;
public $requestParams;
const JWT_SECRET_KEY = "8dc2d81404c4ca0ba23b85b941696534";
public function __construct()
{
$this->requestMethod = $_SERVER["REQUEST_METHOD"];
$this->request = (array) json_decode(file_get_contents('php://input'), true);
}
public function throwError(int $statusCode, string $message)
{
header('Content-Type: application/json');
echo json_encode([
'error' => [
'status' => $statusCode,
'message' => $message,
],
]);
die;
}
public function validateRequest()
{
if ($_SERVER['CONTENT_TYPE'] != 'application/json') {
$this->throwError(403, 'The only acceptable content type is application/json.');
}
if (!isset($this->request['request_params']) || !is_array($this->request['request_params'])) {
$this->throwError(400, 'The parameters of the request are not defined.');
} else {
$this->requestParams = $this->request['request_params'];
}
}
public function validateParams($fieldName, $value, $isRequired = false)
{
if ($isRequired == true && $value == "") {
$this->throwError(400, "The $fieldName field is required.");
}
return $value;
}
public function returnResponse($statusCode, $message)
{
header('Content-Type: application/json');
header("Status: $statusCode");
$http = [
100 => '100 Continue',
101 => '101 Switching Protocols',
200 => '200 OK',
201 => '201 Created',
202 => '202 Accepted',
203 => '203 Non-Authoritative Information',
204 => '204 No Content',
205 => '205 Reset Content',
206 => '206 Partial Content',
300 => '300 Multiple Choices',
301 => '301 Moved Permanently',
302 => '302 Found',
303 => '303 See Other',
304 => '304 Not Modified',
305 => '305 Use Proxy',
307 => '307 Temporary Redirect',
400 => '400 Bad Request',
401 => '401 Unauthorized',
402 => '402 Payment Required',
403 => '403 Forbidden',
404 => '404 Not Found',
405 => '405 Method Not Allowed',
406 => '406 Not Acceptable',
407 => '407 Proxy Authentication Required',
408 => '408 Request Time-out',
409 => '409 Conflict',
410 => '410 Gone',
411 => '411 Length Required',
412 => '412 Precondition Failed',
413 => '413 Request Entity Too Large',
414 => '414 Request-URI Too Large',
415 => '415 Unsupported Media Type',
416 => '416 Requested Range Not Satisfiable',
417 => '417 Expectation Failed',
500 => '500 Internal Server Error',
501 => '501 Not Implemented',
502 => '502 Bad Gateway',
503 => '503 Service Unavailable',
504 => '504 Gateway Time-out',
505 => '505 HTTP Version Not Supported',
];
echo json_encode([
'response' => [
'code' => $http[$statusCode],
'message' => $message,
],
]);
die;
}
public function generateToken(int $userId)
{
try {
$token = JWT::encode([
'iat' => time(),
'iss' => 'localhost',
'exp' => time() + 60 * 60,
'userId' => $userId,
], self::JWT_SECRET_KEY);
} catch (\Exception $e) {
echo $e->getMessage();
}
$this->returnResponse(200, ['token' => $token]);
}
public function notfound()
{
$this->returnResponse(404, 'The endpoint you are looking for is not found.');
}
}
آنچه در این کلاس تغییر کرده آن است که در خط چهارم کلاس JWT
را به اصطلاح use
کردهایم چرا که قرار است تا از آن داخل متدی تحت عنوان ()generateToken
استفاده نماییم. در خط دوازدهم نیز یک کانستَنت تحت عنوان JWT_SECRET_KEY
ساختهایم و مقدار آن را برابر با یک استرینگ نسبتاً طولانی و غیرقابلحدس دلخواه قرار داده سپس در انتهای این کلاس متدی تحت عنوان ()generateToken
حاوی کدهای زیر ساختهایم:
public function generateToken(int $userId)
{
try {
$token = JWT::encode([
'iat' => time(),
'iss' => 'localhost',
'exp' => time() + 60 * 60,
'userId' => $userId,
], self::JWT_SECRET_KEY);
} catch (\Exception $e) {
echo $e->getMessage();
}
$this->returnResponse(200, ['token' => $token]);
}
میبینیم که از ساختار try
و catch
استفاده کردهایم و دلیلش هم آن است که ممکن است در حین ساخت توکن توسط کلاس JWT
با مشکلی مواجه شویم. در واقع، چنانچه پروسهٔ ساخت توکن بدون هیچ گونه مشکلی پیش رود، توکن ساختهشده را داخل متغیری تحت عنوان token$
ذخیره میسازیم و در غیر این صورت وارد بلوک catch
شده و متد ()getMessage
از کلاس Exception
را روی آبجکت ساختهشده از روی این کلاس تحت عنوان e$
فراخوانی کرده و مقدار آن را چاپ میکنیم. در نهایت هم با استفاده از متد ()returnResponse
یک کد وضعیت مرتبط همچون 200 به علاوهٔ کلیدی تحت عنوان token
که مقدارش همان متغیر token$
است را ریترن میکنیم.
در ارتباط با نحوهٔ ساخت توکن با استفاده از کلاس JWT
نیز باید گفت که داخل این کلاس متدی از جنس static
داریم تحت عنوان ()encode
که حداقل دو پارامتر ورودی الزامی دارا است بدین شکل که پارامتر اول آرایهای است از یکسری مقادیر و پارامتر الزامی دوم نیز همان کانستَنت JWT_SECRET_KEY
است که اصطلاحاً تحت عنوان Secret شناخته میشود.
نکته |
متدهای استاتیک را میتوان با استفاده از علائم :: پس از نام کلاس فراخوانی کرد. |
در ارتباط با پارامتر اول که از جنس آرایه است باید گفت که کلید iat
برگرفته از واژگان «Issue At» است که به نوعی مرتبط با زمان ساخت این توکن میباشد و همانطور که ملاحظه میشود، برای مقدار این کلید از تابع ()time
استفاده نمودهایم. کلید بعد iss
برگرفته از واژهٔ «Issuer» میباشد که قرار است تا تولیدکنندهٔ این توکن را در خود ذخیره سازد که در این مثال از استرینگ localhost
استفاده کردهایم. کلید exp
از واژهٔ «Expiration» گرفته شده و دربرگیرندهٔ تاریخ انقضاء این توکن است و همانطور که میبینیم، به عنوان تاریخ انقضاء این توکن از تابع ()time
به علاوهٔ 60 * 60
استفاده کردهایم؛ به عبارتی، این توکن از زمان ایجاد فقط یک ساعت معتبر خواهد بود و در نهایت هم کلیدی دلخواه همچون user_id
در نظر گرفته و مقدار آن را برابر با پارامتر ورودی این متد تحت عنوان userId$
در نظر گرفتهایم. کل این آرایه به عنوان Payload در نظر گرفته شده سپس توسط الگوریتمهای به کار رفته در این لایبرری هَش میشود.
روش تست قابلیت لاگین کردن کاربران
در این مرحله از کار، میتوان با در نظر گرفتن اِندپیونت api/v1/auth
نحوهٔ لاگین کردن به سیستم را تست کرد. برای این منظور، همچون نحوهٔ ثبتنام یک کاربر جدید، ابتدا متد POST
را انتخاب نموده سپس در تَب مربوط به Headers نرمافزار Postman هِدِری تحت عنوان Content-Type
با مقدار application/json
سِت کرده و در ادامه روی تَب Body کلیک کرده فرمت جیسونی همچون آنچه در تصویر زیر مشاهده میکنید وارد مینماییم:
در واقع، با در نظر گرفتن دقیقاً همان ایمیل و پسوردی که در حین ثبت کاربر جدید انتخاب نمودیم و با کلیک بر روی دکمهٔ Send میبینیم که کلید token
حاوی یک JWT است که از این پس با استفاده از آن میتوانیم این پیام را به سرور بدهیم که کلاینتِ معتبری هستیم. در عین حال، میتوانیم صحت عملکرد متد ()auth
را با انجام یکسری تغییرات در ریکوئست ارسالی تست نماییم. به طور مثال، اگر نوع متد را از POST
به متدی همچون GET
تغییر دهیم، با ارور زیر مواجه خواهیم شد:
{
"error": {
"status": 405,
"message": "The only method that is acceptable is POST."
}
}
همچنین اگر در تَب Headers تیک گزینهٔ Content-Type
را بر داریم و درخواست خود را ارسال کنیم، با ارور زیر مواجه خواهیم شد:
{
"error": {
"status": 403,
"message": "The only acceptable content type is application/json."
}
}
اگر هم در بخش Body جسیون مذکور به صورت زیر فاقد کلید pass
باشد:
{
"request_params": {
"mail": "hi@example.com"
}
}
با ارور زیر مواجه خواهیم شد:
{
"error": {
"status": 400,
"message": "The pass field is required."
}
}
میبینیم که متد ()validateParams
به درستی پیامی مبنی بر اجباری بودن فیلد pass
در معرض دیدمان قرار داده است.
جمعبندی
در این آموزش با تکمیل کلاس UserController
قابلیت لاگین در این وب سرویس و دریافت توکنی از جنس JWT را اضافه نمودیم به طوری که از این پس کلاینت صرفاً نیاز خواهد داشت تا توکن مذکور را در اختیار وب سرویس قرار داده و به اِندپوینتهایی که سطح دسترسی محدود دارند دست پیدا کند.