احراز هویت کاربران
در این بخش میخواهیم راجع به احراز هویت (authentication) صحبت کنیم.
احراز هویت پروسه ای هستش که در اون کاربر میتونه برای یک حساب جدید ثبت نام کنه، وارد حسابش بشه و از حسابش خارج بشه.
پیاده سازی پروسه ی احراز هویت در یک پروژه ی تماما جنگویی نسبتا ساده است ولی در خصوص API ، اوضاع یکم فرق داره. اگر یادتون باشه قبلا گفتیم که HTTP یک پروتکل بدون حالت یا stateless هستش، پس هیچ روش توکاری برای بخاطر سپردن وضعیت احراز هویت شدگی یک کاربر از درخواستی به درخواست دیگه وجود نداره. به عبارت دیگه، هر باری که یک کاربر بخواد درخواستی رو برای دسترسی به یک منبع تحت نظارت و محدود شده ارائه بده، باید هویتش تایید بشه.
راه حلش اینه که یک شناسه ی منحصربفرد رو در کنار هر کدوم از درخواست های HTTP ، ضمیمه کنیم. اما سوالی که پیش میاد اینه که این شناسه باید به چه شکلی باشه؟ خب باید بگم که هیچ رویکرد تایید شده ی بین المللی در این باره وجود نداره و این شناسه میتونه اَشکال متفاوتی داشته باشه. اما نیازی به نگرانی نیست چون DRF با در نظر گرفتن این قضیه میاد و چهار تا گزینه به ما ارائه میده:
- Basic Authentication
- Session Authentication
- Token Authentication
- Default Authentication
در این بخش سراغ هر چهار تا گزینه بالا میریم و مزایا و معایب هر رویکرد رو بررسی میکنیم تا نهایتا یک انتخاب آگاهانه در رابطه با پروژه خودمون داشته باشیم. علاوه بر این، در این بخش قراره که اِندپوینت هایی رو بمنظور ثبت نام، ورود و خروج کاربران ایجاد کنیم.
Credentials
ترجمه واژه Credentials به فارسی به معنی استوارنامه یا اعتبارنامه است اما این واژه در حوزه فناورى اطلاعات به مفهوم گستردهترى اشاره داره. واژه Credentials به مفهوم چیزیه که با استفاده از آن شما احراز هویت میشید و در واقع به وسیله ارائه دادن Credentials هستش که بقیه تشخیص میدن که شما خودتونید و کسی هویت شما رو جعل نکرده.
از این رو بهتره که واژه Credentials رو در فارسی به عنوان ابزار احراز هویت بشناسیم. حالا این ابزارهای احراز هویت انواع و اقسام دارند: نظیر رمزهای عبوری (رمز عددی، اثر انگشت، اسکن صورت، ... ) که همه روزه برای استفاده از کامپیوتر، گوشی و کارت اعتباری از آنها استفاده میکنیم و ابزارهای احراز هویتی که بصورت فیزیکی در اختیار داریم مانند کارتهای اعتباری بانکی، شناسنامه، کارت ملی و ... .
در ادامه مطالب هر جا که گفتیم Credentials منظورمون بیشتر اون دسته از ابزاری هستن که در بستر اینترنت بمنظور تایید هویت خودمون ازشون استفاده میکنیم و اغلب در قالب هِدر های HTTP ( HTTP headers) ارسالشون میکنیم.
Basic Authentication
این رویکرد، رایج ترین رویکرد احراز هویت پروتکل HTTP هستش. وقتیکه مشتری(client) یک درخواست HTTP ارائه میده، قبل از اینکه بهش اجازه دسترسی داده بشه باید یکسری credential بفرسته.
این روند درخواست/پاسخ بدین صورت اتفاق میفته که :
- کلایِنت یک درخواست HTTP ایجاد میکنه
- سرور پاسخی حاوی کد وضعیت 401 (احراز هویت نشده) رو به همراه هِدِری با عنوان " WWW-Authenticate " میفرسته که جزئیات نحوه ی تایید هویت رو به کلایِنت اطلاع میده
- کلایِنت یکسری credential رو از طریق هِدِر Authorization پس میفرسته
- سرور این credential ها رو بررسی میکنه و با یکی از کد های وضعیت 200(Ok) یا 403(Forbidden) جوابش رو میده
در صورت تایید شدن، کلایِنت همه ی درخواست های بعدیش رو به ضمیمه ی هِدِر Authorization میفرسته. کل این چرخه رو میتونید به شکل زیر متجسم بشید:
توجه داشته باشید که credential های ارسالی ما بصورت غیر رمزنگاری شده هستن: " <username>:<password> " و توسط الگوریتم رمزنگاری base64 به فرم دیگه ای درمیان؛ بعنوان مثال در خصوص خود من: " admin:codingcogs " که توسط این الگوریتم به شکل مقابل در میاد: " ==YWRtaW46Y29kaW5nY29ncw " .
مزیت اصلی این روش ساده بودنشه اما چند تا عیب داره. اول اینکه سرور باید تک تک درخواست ها رو از بابت درست بودن username و password تایید کنه که به نوبه ی خودش ناکار آمد هستش و خیلی بهتر میشد اگه فقط یه بار چک میکرد و بعدش یه چیزی مثل توکِن(token) میداد بهش تا نشانگر این باشه که این کاربر مورد تایید هستش. عیب دومش اینه که credential ها رو میشه به راحتی رمزگشایی کرد! خودتون میتونید برای امتحان کردنش به وبسایت: " /https://www.base64decode.org " برید و با وارد کردن " ==YWRtaW46Y29kaW5nY29ncw " به راحتی رمزگشاییش کنید. پس بهتره که از این رویکرد فقط از طریق HTTPS استفاده کنید که در واقع نسخه ی رمزگذاری شده ی HTTP هستش.
***: به توکن به دید یک کلید دیجیتال نگاه کنید.
Session
بطور خلاصه، Session به مجموعه ای از فعل و انفعالات کاربر با یک وبسایت در یک قالب زمانی معین گفته میشه؛ بعنوان مثال: یک session میتونه شامل بازدید از چندتا صفحه، پرداخت های الکترونیکی و ... بشه. اطلاعات مربوط به تنظیمات یک session در داخل یک session object و روی سرور ذخیره میشه که در عوضش یک session ID تولید و در اختیار کلایِنت(مرورگر کاربر) قرار میگیره.
Session Authentication
وبسایت های جنگویی برای مدت ها از یک رویکرد جایگزین استفاده میکردن که ترکیبی از session و cookie بود. بدین صورت که کاربر با استفاده از credential های خودش احراز هویت میشه و سپس یک session ID از سرور دریافت میکنه که در قالب یک کوکی(cookie) ذخیره میشه و در همه ی درخواست های بعدی کاربر ضمیمه میشه.
وقتیکه این session ID در ضمنِ یک درخواست ارسال میشه، سرور با استفاده از اون دنبال یک session object میگرده که شامل همه ی اطلاعات مربوط به یک کاربر هستش.(این اطلاعات شامل credential ها هم میشه)
این رویکرد به اصطلاح stateful یا حالت دار هستش، چون باید یکسری داده رو هم در طرف سرور (بعنوان session object) و هم در طرف کلایِنت (بعنوان session ID) حفظ و نگه داری کرد.
روند کلی بدین شکل هستش که:
- یک کاربر میاد و credential های خودش رو وارد میکنه
- سرور صحت این credential ها رو تایید و یک session object تولید میکنه که بعدا روی دیتابیس ذخیره میشه
- یک session ID از طرف سرور به کلایِنت ارسال میشه که به عنوان یک کوکی در مرورگر ذخیره میشه
- این session ID به عنوان یک هِدِر HTTP در همه ی درخواست های بعدی درج میشه که اگر دیتابیس تاییدش کنه، پردازش پروسه ی مورد نظر ادامه پیدا میکنه
- وقتیکه کاربر از وب اَپلیکیشن خارج میشه، این session ID هم توسط کلایِنت و هم توسط سرور از بین میره
- اگر کاربر بعدا دوباره لاگین کنه، یک session ID جدید تولید میشه و به عنوان یک کوکی در سمت کلاِینت ذخیره میشه
تنظیمات پیشفرض DRF ترکیبی از دو رویکرد Basic Authentication و Session Authentication هستش. مزیت این رویکرد ترکیبی در ایمن و کارآمد بودنشه، چون credential ها فقط یکبار ارسال میشن و دیگه مثل Basic Authentication نیازی نیست که هر بار ارسال بشن. علاوه بر این، دیگه نیازی نیست که سرور هر دفعه بیاد و credential ها رو تایید کنه و بجاش فقط چک میکنه که آیا session ID با session object متناظره یا نه .
این رویکرد معایب خودش هم داره. اولیش اینه که session ID فقط در داخل مرورگری اعتبار داره که با اون لاگین کرده باشیم و به همین خاطر برای چند تا دامنه متفاوت پاسخگو نیست. این مشکل اونجایی خیلی به چشم میاد که شما یک API داشته باشید و بخواهید از چند تا فرانت-اِند(مثل یک وبسایت و یک اَپلیکیشن موبایل) پشتیبانی کنید. عیب دوم این رویکرد اینه که session object باید به روز نگه داشته بشه که این قضیه در خصوص سایتهای بزرگی که چند تا سرور دارن چالش سختی به حساب میاد. عیب سومش هم اینه که داریم برای هر درخواستی کوکی میفرستیم، حتی درخواست هایی که نیازی به احراز هویت ندارن و این قضیه به خودی خود کارایی رو پایین میاره.
خلاصه این که استفاده از این رویکرد برای API هایی که از چند تا فرانت-اِند پشتیبانی میکنن توصیه نمیشه.
Cookie و LocalStorage
کوکی ها بمنظور مشاهده و خواندن اطلاعات سمت سرور مورد استفاده قرار میگیرن و بطور خودکار همراه با همه ی درخواست های HTTP ارسال میشن.( سایز کوکی ها 4KB هستش)
حافظه ی محلی یا LocalStorage برای اطلاعات سمت کلایِنت طراحی شده و محتوایی که داره رو بصورت پیشفرض با همه ی درخواست های HTTP نمیفرسته.( سایز حافظه داخلی 10/5 MB هستش)
Token Authentication
حالا میرسیم به سومین رویکرد که قراره در پروژه خودمون هم همین رو پیاده کنیم.
این رویکرد در سالهای اخیر تبدیل به محبوبترین رویکرد احراز هویت شده و علتش هم برمیگرده به پیدایش اَپلیکیشن های تک صفحه ای یا همون SPA ها.
احراز هویت مبتنی بر توکن، جزو دسته ی stateless یا بدون حالت هستش: وقتیکه کلایِنت برای اولین بار میاد و credential های کاربر رو به سرور ارسال میکنه، یک توکن منحصربفرد تولید میشه و در قالب یک کوکی یا یک حافظه ی محلی در سمت کلایِنت ذخیره میشه. سپس این توکن در بین هِدِر های همه ی درخواست های HTTP درج میشه و سرور با استفاده ازش، هویت یک کاربر رو تایید میکنه.
یک مثال ساده از این روند رو درشکل زیر مشاهده میکنید. توجه داشته باشید که هدرِ " WWW-Authenticate " مشخص میکنه که قراره از یک توکن استفاده بشه و این توکن رو در پاسخ متناظر با خودش و در هدرِ " Authorization " مشاهده میکنید.
این رویکرد چند تا مزیت داره. از اونجائیکه توکن ها در سمت کلایِنت ذخیره میشن، دیگه از بابت بروز نگه داشتن session object مشکلی نخواهیم داشت. همچنین میتونیم توکن ها رو بین چند تا فرانت-اِند به اشتراک بگذاریم یعنی میتونیم با استفاده از یک توکن، کاربری رو هم در وبسایت و هم در اَپلیکیشن موبایل نمایش بدیم.(این مورد شامل session ID نمیشه)
یک نقطه ضعف بالقوه برای این رویکرد اینه که سایز توکن ها میتونه خیلی بزرگ بشه. همونطوریکه گفته شد، توکن به همراه همه ی درخواست ها ارسال میشه و به همین خاطر مدیریت سایز توکن میتونه تبدیل به یک مشکل بشه.
نحوه ی پیاده سازی توکن ها نسبت به همدیگه میتونه بشدت متفاوت و پیچیده باشه اما در مورد TokenAuthentication توکارِ DRF اینطوری نیست و تعمدا به شکلی کاملا ابتدایی پیاده شده. به همین خاطر دیگه نمیشه برای توکن ها زمانِ انقضا تعیین کرد، که به نوبه ی خودش یک پیشرفت امنیتی محسوب میشه که میتونستیم به توکن ها اضافه اش کنیم. علاوه بر این فقط میتونید یک توکن برای یک کاربر تولید کنید؛ یعنی یک کاربر چه روی وبسایت باشه و چه روی اَپلیکیشن موبایل، فقط میتونه از یک توکن یکسان استفاده کنه و از اونجائیکه اطلاعات کاربر بصورت محلی(local) ذخیره میشه، این مورد میتونه مشکلاتی رو از بابت نگه داری و بروزرسانی دو مجموعه ی اطلاعاتی بوجود بیاره.
JSON Web Tokens (JWTs)
این توکن ها گونه ی جدیدتری از توکن هستند که شامل داده هایی به فرمت JSON با امضای رمزنگاری شده میشن. JWT ها در اصل بمنظور استفاده در OAuth طراحی شدند: روشی با استاندارد باز بمنظور اشتراک گذاری اطلاعات کاربر بین وبسایتها، بدون درج رمز عبور کاربر.
این JWT ها رو میشه در سمت سرور و با استفاده از پکیج هایی مثل: djangorestframework-simplejwt یا سرویس هایی مثل Auth0 تولید کرد. با این وجود، توسعه دهنده ها در مورد مزایا و معایب استفاده از JWT در خصوص احرازهویت کاربر اختلاف نظر هایی دارن که پرداختن بهش خارج از حوصله این دوره هستش.
*: میتونید JWT بنویسیدش و " jot " تلفظش کنید.
Default Authentication
مطمئنا هر پکیجی واسه خودش تنظیماتی داره که از قبل براش پیکربندی شدن و این قضیه در مورد DRF هم صدق میکنه. DRF یکسری تنظیمات پیشفرض داره که بصورت غیر مستقیم و به دور از چشم ما پیکربندی شدن و ما با توجه به نیازهای پروژه ی خودمون میتونیم تغییرشون بدیم و مواردی رو بهشون اضافه کنیم، مثل: DEFAULT_PERMISSION_CLASSES که بصورت پیشفرض روی AllowAny قرار داشت و ما در بخش قبل به IsAuthenticated تغییرش دادیم. حالا چرا میگیم به دور از چشم ما ؟ در واقع این تنظیمات رو میتونید با مراجعه به سورس کد DRF در Github و با یکم جستجو کردن پیداشون کنید ولی اگر خواستید تغییرشون بدید، باید صراحتا در ضمن کدهای پروژه خودتون بیاریدشون و سپس تغییرات مورد نظرتون رو روش اعمال کنید.
با علم به این موضوع، میخواهیم که تنظیمات پیشفرض DEFAULT_AUTHENTICATION_CLASSES رو تغییر بدیم. پس برای اینکار باید ابتدا صراحتا این تنظیمات پیشفرض رو در کد های پروژه خودمون بیاریم.
فایل django_project/settings.py رو باز کنید تا خطوط جدیدی رو بهش اضافه کنیم:
Code
# django_project/settings.py
…
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_AUTHENTICATION_CLASSES": [ # new
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
],
}
…
حالا چرا از هر دو متد استفاده شده؟ چون از هر کدوم به خاطر هدف معینی استفاده میشه. از اولی بمنظور بالا آوردن Browsable API و امکان وارد و خارج شدن ازش استفاده میشه و از دومی هم بمنظور پاس دادن session ID به هِدر های HTTP استفاده میشه.
اگر سری به آدرس: " /http://127.0.0.1:8000/api/v1 " بزنید میبینید که همه چیز طبق روال سابق داره کار میکنه، چون از از لحاظ فنی هیچ تغییری ایجاد نشده و ما فقط تنظیمات پیشفرض رو صراحتا در کدهای خودمون آوردیم.
پیاده سازی Token Authentication
حالا وقتش رسیده که سیستم احراز هویت خودمون رو بروزرسانی کنیم تا از توکن ها استفاده کنه. اولین قدم ما بروزرسانی تنظیمات DEFAULT_AUTHENTICATION_CLASSES هست تا بیاد و از TokenAuthentication استفاده کنه. یکم قبل تر تنظیمات پیشفرض رو صراحتا اضافه کردیم و الان میخواهیم که تغییرات مورد نظر خودمون رو بهش اعمال کنیم.
Code
# django_project/settings.py
…
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication", # new
],
}
…
به SessionAuthentication دست نزدیم چون برای Browsable API نیازش داریم، اما الان از توکن ها استفاده میکنیم تا credential های لازم برای احرازهویت رو در هِدر های HTTP درج کنیم. همچنین لازمه داریم که اَپ " authtoken " رو که کارش تولید توکن در سرور هستش رو به قسمت INSTALLED_APPS اضافه کنیم:
Code
# django_project/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd-party apps
'rest_framework',
'corsheaders',
'rest_framework.authtoken', # new
# Local
'accounts',
'posts',
]
از اونجائیکه تغییراتی رو در INSTALLED_APPS ایجاد کردیم، لازمه که دیتابیس خودمون رو همگام با این تغییرات نگه داریم. سرور خودتون رو با کلید های Ctrl+C متوقف کنید و دستور زیر رو اجرا کنید:
Shell
(blogapi-JvRm-7-t) > python manage.py migrate
Operations to perform:
Apply all migrations: accounts, admin, auth, authtoken, contenttypes, posts, sessions
Running migrations:
Applying authtoken.0001_initial... OK
Applying authtoken.0002_auto_20160226_1747... OK
Applying authtoken.0003_tokenproxy... OK
حالا دوباره سرور رو راه بندازید:
Shell
(blogapi-JvRm-7-t) > python manage.py runserver
اگر به پنل اَدمین در آدرس: " /http://127.0.0.1:8000/admin " سربزنید، مشاهده میکنید که یک قسمت با نام " Tokens " به پنل اضافه شده.
روی لینک " Tokens " کلیک کنید.
همینطوریکه مشاهده میکنید، فعلا هیچ توکنی نداریم. البته درسته که ما همین الانش هم دو تا کاربر داریم، ولی توکن ها فقط بعد از اینکه یک کاربر لاگین کنه تولید میشن. یکم جلوتر که بریم به اونجاش هم میرسیم!
اِندپوینت ها
حالا لازمه که اِندپوینت هایی رو بمنظور وارد/خارج شدن اضافه کنیم. میتونیم برای اینکار یک اَپ اختصاصی به نام " users " ایجاد کنیم و بعدش براش url و view و serializer اضافه کنیم. اما مطمئنا دلمون نمیخواد که در حوزه ی احراز هویت کاربران دچار هیچگونه اشتباهی بشیم. و از اونجائیکه تقریبا همه ی API ها به این عملکرد نیاز دارن، منطقی بنظر میرسه که یکسری پکیج های تست شده و کارآمد برای این منظور وجود داشته باشن که ما هم بتونیم ازشون استفاده کنیم.
به همین خاطر ما قراره که از پکیج dj-rest-auth در ترکیب با django-allauth استفاده کنیم تا کارها ساده تر بشن. راجع به استفاده از این پکیج ها (third-party package) احساس گناه نداشته باشید، چون بالاخره به یه دلیلی وجود دارن و حتی بهترین متخصص های جنگو هم به این پکیج ها اتکا میکنن.
dj-rest-auth
در ابتدا میخواهیم که اِندپوینت هایی رو برای وارد شدن، خارج شدن و بازیابی رمز عبور اضافه کنیم که پکیج محبوب dj-rest-auth این موارد رو در خودش داره. سرور رو با کلید های Ctrl+C متوقف کنید و سپس پکیج رو نصب کنید:
Shell
(blogapi-JvRm-7-t) > pipenv install dj-rest-auth==2.1.11
حالا این اَپ جدید رو به قسمت INSTALLED_APPS در فایل django_project/settings.py اضافه اش میکنیم:
Code
# django_project/settings.py
…
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd-party apps
'rest_framework',
'corsheaders',
'rest_framework.authtoken',
'dj_rest_auth',
# Local
'accounts',
'posts',
]
…
حالا باید URL های پکیج dj-rest-auth رو به فایل djagno_project/urls.py اضافه کنیم. مسیری که براشون انتخاب میکنیم: " api/v1/dj-rest-auth " خواهد بود. دقت کنید که به جای علامتِ: " – " از "_" استفاده نکنید.
Code
# django_project/urls.py
…
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/v1/", include("posts.urls")),
path("api-auth/", include("rest_framework.urls")),
path("api/v1/dj-rest-auth/", include("dj_rest_auth.urls")),
]
…
همین!
حالا اگه میخواستیم اِندپوینت های خودمون رو بمنظور احراز هویت کاربران پیاده کنیم باید کلی وقت و حوصله میذاشتیم و دقت میکردیم که مرتکب کوچکترین اشتباهی نشیم، اما با استفاده از پکیج dj-rest-auth از هیچکدوم از این چیزا خبری نیست و کلی جلو میفتیم.
سرور رو بالا میاریم تا ببینیم dj-rest-auth برامون چیکارا کرده:
Shell
(blogapi-JvRm-7-t) > python manage.py runserver
ابتدا میریم سراغ اِندپوینت لاگین که در آدرس: " /http://127.0.0.1:8000/api/v1/dj-rest-auth/login " قرار داره.
بعدش سراغ اِندپوینت لاگ اوت در آدرس: " /http://127.0.0.1:8000/api/v1/dj-rest-auth/logout " میریم.
بعدش، اِندپوینت بازیابی رمز عبور رو در آدرس: " /http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset " چک میکنیم
یک اِندپوینت هم برای تایید رمز بازیابی شده در آدرس: " /http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset/confirm " داریم.
ادامه روند احراز هویت رو در بخش بعدی با اضافه کردن قابلیت ثبت نام کاربران ادامه میدیم.