مجوز های دسترسی
امنیت یکی از مهمترین مفاهیم در خصوص وبسایت ها هستش که در رابطه با web API ها اهمیتش دو چندان میشه. در حال حاضر API ما به همه دسترسی کامل میده. هیچ محدودیتی نیست؛ هر کاربری میتونه دست به هر کاری بزنه که بشدت خطرناکه! بعنوان مثال: یک کاربر ناشناس میتونه هر پستی رو بخونه، ایجاد کنه، ویرایش و یا حذف کنه. حتی پستی رو که خودش ایجاد نکرده باشه! مسلمه که همچین چیزی اصلا باب میل ما نیست.
خوشبختانه DRF یکسری تنظیمات در رابطه با مجوز های دسترسی داره که میتونیم برای حفاظت از API خودمون بکار بگیریمشون. این تنظیمات رو میشه در سه سطح اعمالشون کرد:
- project-level
- view-level
- model-level
در این بخش، هر سه مورد رو بررسی میکنیم و نهایتا یک مجوز دسترسی سفارشی برای خودمون ایجاد میکنیم تا فقط نویسنده ی یک پست بتونه ویرایش یا حذفش کنه.
مجوز های Project-Level
خود DRF مجموعه ای از پیکربندیها رو داره که در تنظیمات جنگو و در قسمت REST_FRAMEWORK قرار میگیرن. ما قبلا یکیشون رو صراحتا در فایل django_project/setting.py آوردیم:
Code
# django_project/settings.py
…
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
}
…
در اصل چهار تا از این تیپ پیکربندی های توکار داریم که در دسته ی project-level قرار میگیرن:
- AllowAny : هر کاربری، چه احراز هویت شده باشه و چه نشده باشه، دسترسی کامل داره
- IsAuthenticated : فقط کاربرانی دسترسی دارن که ثبت نام کردن و احراز هویت شدن
- IsAdminUser : فقط اَدمین ها یا superuser ها دسترسی دارن
- IsAuthenticatedOrReadOnly : کاربرانی که احراز هویت نشدن میتونن هر صفحه ای رو ببینن ولی فقط اونهایی که احراز هویت شده باشن از امتیازات ایجاد، ویرایش یا حذف کردن برخوردارن.
بمنظور پیاده سازی هر کدوم از موارد بالا کافیه که تنظیمات DEFAULT_PERMISSION_CLASSES رو بروزرسانی کنیم و مرورگر خودمون رو refresh کنیم. همین!
از این بین، ما فقط سوییچ میکنیم روی IsAuthenticated تا فقط کاربرانی که احراز هویت شدن یا وارد شدن، بتونند از API ما دیدن کنن.
این بخش از تنظیمات در فایل django_project/settings.py رو به شکل زیر در میاریم:
Code
# django_project/settings.py
…
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated", # new
],
}
حالا اگه مرورگر خودتون رو refresh کنید میبیند که هیچ اتفاقی نمیفته و علتش اینه که ما با حساب superuser وارد شدیم که اگه به گوشه بالا-راست صفحه نگاه کنید میتونید از این بابت مطمئن بشید. بمنظور خروج از حساب فعلی به آدرس: " /http://.127.0.0.1:8000/admin " برید و روی لینک " Log Out " در گوشه بالا-راست کلیک کنید.
حالا اگه دوباره به آدرس: " /http://127.0.0.1:8000/api/v1 " برگردید، با کد 403 مواجه میشید که از عدم ورود مشخصات لازم بمنظور احراز هویت کاربر حکایت داره و این دقیقا همون چیزیه که میخواستیم!
ایجاد کاربر جدید
لازمه که شرایط دسترسی به API خودمون رو از دید یک کاربر عادی بررسی کنیم چون بالاخره قرار نیست که همه اَدمین باشن! برای ایجاد کاربر جدید دوباره به آدرس: " /http://127.0.0.1:8000/admin " برگردید و وارد حساب superuser خودتون بشید. بعدش روی لینک " +Add " روبه روی Users کلیک کنید. سپس یک نام کاربری و رمزعبور برای کاربر جدید وارد کنید و روی دکمه ی " Save " کلیک کنید. من از نام کاربری: testuser و رمزعبور: testpass1234 کردم ولی شما میتونید از مشخصات دلخواه خودتون استفاده کنید. همچنین توجه داشته باشید که یک فیلد با عنوان " Name " هم داریم که پرکردنش اختیاریه و به لطف مدل کاربری سفارشی خودمون در اینجا حضور داره.
در صفحه ی بعدی میتونیم علاوه بر نام کاربری و رمزعبور، یکسری اطلاعات دیگه هم راجع به کاربر جدیدمون اضافه کنیم ولی لازم نیست و همینی که هست کفایت میکنه.
به انتهای صفحه برید و روی دکمه ی " Save " کلیک کنید. بعدش به یک صفحه جدید هدایت میشید که میتونید لیستی از کاربرها رو مشاهده کنید.
در این صفحه اگر به " Staff Status " دقت کنید متوجه میشید که فقط یکی از حساب ها superuser هستش و اون یکی صرفا یه حساب عادیه.
نهایتا روی لینک " Log Out " در گوشه بالا-راست صفحه کلیک میکنیم تا از پنل اَدمین خارج بشیم.
اضافه کردن قابلیت های Log In و Log Out
حالا سوالی که پیش میاد اینه که این کاربر جدید چجوری قراره لاگین کنه (Log In) ؟ خوشبختانه DRF تقریبا ترتیب همه چیز رو از این بابت داده و ما میتونیم فقط با اضافه کردن یک خط در فایل django_project/urls.py ، یک لینک " Log In " داشته باشیم.
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")), # new
]
اجباری نبود که از الگوی " /api-auth " استفاده کنیم و میتونستیم هر چیز دیگه ای هم بجاش وارد کنیم ولی ترجیحا طبق مستندات DRF جلو رفتیم.
حالا اگه در مرورگر خودتون به آدرس: " /http://127.0.0.1:8000/api/v1 " برید، یک تغییر جزئی میبیند:
یک لینک با عنوان " Log In " در گوشه بالا- راست صفحه اضافه شده.
روی این لینک جدید کلیک کنید و با وارد کردن مشخصات حساب جدیدی که ایجاد کردیم وارد بشید.
بعد از ورود، به صفحه لیست بندی پست ها هدایت میشید. اگه به گوشه بالا-راست صفحه نگاه کنید نام کاربری حساب جدیدتون رو مشاهده میکنید که کنارش یک فلش رو به پایین وجود داره که اگه روش کلیک کنید، لینک " Log Out " براتون نمایش داده میشه.
مجوز های View-Level
مجوز هایی در این سطح، امکان کنترل جزئی تر و دقیق تری رو برامون فراهم میکنن.
در حال حاضر دو تا view در فایل posts/views.py داریم که ما قصد داریم دومیش یعنی: " PostDetail " رو به نحوی بروزرسانی کنیم که فقط اَدمین ها بتونن ببیننش. در صورتی که اینکار بدرستی پیاده بشه: کاربری که لاگین نکرده کلا نمیتونه API ما رو ببینه، کاربری که لاگین کرده فقط میتونه لیست پست ها رو ببینه، و فقط اَدمین میتونه صفحه ی اختصاصی هر پست رو ببینه.
پس در ابتدای فایل مذکور میایم و permissions رو از DRF فراخوانی میکنیم. سپس فیلد permission_classes رو به کلاسِ PostDetail اضافه اش میکنیم و این فیلد جدید رو روی IsAdminUser تنظیمش میکنیم.
Code
# posts/views.py
from rest_framework import generics, permissions # new
from .models import Post
from .serializers import PostSerializer
class PostList(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAdminUser,) # new
queryset = Post.objects.all()
serializer_class = PostSerializer
حالا اگه مرورگر خودتون رو refresh کنید میبیند که لیست پست ها هنوزم براتون نمایش داده میشه ولی اگر به صفحه اختصاصی پست اول در آدرس: " /http://127.0.0.1:8000/api/v1/1 " برید، با کد وضعیت 403 روبه رو میشید!
اگر از حساب فعلی خارج بشید و این دفعه با حساب اَدمین وارد بشید، دیگه با این پیام مواجه نمیشید چون دسترسی کامل دارید. پس میشه گفت که ما بطرز صحیحی یک مجوزِ view-level رو پیاده کردیم.
بطور کلی، انواع استاندارد مجوز ها عبارتند از: دسترسی کامل برای همه، دسترسی کامل برای کاربران احراز هویت شده، دسترسی محدود شده برای کاربران احراز هویت شده، و دسترسی کامل برای اَدمین ها. پیکربندی مجوز ها، بستگی به نیازهای پروژه شما داره.
حالا قبل از اینکه ادامه بدیم، دوباره به فایل posts/views.py برگردید و permissions رو از ابتدای فایل، به همراه فیلد permission_classes از کلاسِ PostDetail حذف کنید. چون برای کاری که ما میخواهیم بکنیم، همین که فقط کاربران احرازهویت شده اجازه دسترسی داشته باشند کافیه. ما اینکار رو قبلا در فایل django_project/settings.py و در تنظیمات DEFAULT_PERMISSON انجامش دادیم.
مجوز سفارشی
حالا میخواهیم اولین مجوز سفارشی خودمون رو درست کنیم تا فقط نویسنده ی یک پست اجازه ی ویرایش یا حذف همون پست رو داشته باشه؛ به عبارت دیگه قراره که یک کاربر عادی فقط مجاز به ویرایش و حذف پست های خودش باشه، اما اَدمین همچنان مجاز به انجام هر کاری خواهد بود.
بصورت توکار، DRF متکی به کلاسی به نام BasePermission هستش که همه ی کلاس های permission بصورت وراثتی ازش استفاده میکنن و کد مربوطه اش رو هم میتونید در Github ببینید:
Code
class BasePermission(metaclass=BasePermissionMetaclass):
"""
A base class from which all permission classes should inherit.
"""
def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
def has_object_permission(self, request, view, obj):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
بمنظور ایجاد یک مجوز سفارشی میشه از هر دو تا متد بالا استفاده کرد.
تا اینجای فصل و در پروژه های مختلف بارها پیش اومده که بخواهیم اطلاعات ورودی رو بصورت لیست شده نمایش بدیم و برای اینکار از view هایی استفاده کردیم که به اصطلاح به: " list-view " معروف هستند (مثل ListAPIView) ؛ حالا اگه بخواهیم برای این دسته از view ها مجوز دسترسی سفارشی تعریف کنیم، میایم و از مِتد has_permission استفاده میکنیم. مشابه همین قضیه، ما یک دسته دیگه هم داریم که به: " detail-view " معروف هستند (مثل RetrieveAPIView) و اگه بخواهیم براشون مجوز سفارشی تعیین کنیم باید از هر دو مِتد has_permission و has_object_permission استفاده کنیم اما نکته اش در اینه که باید اول has_permission اجرا بشه و True برگردونه تا بعدش has_object_permission اجرا بشه.
حالا ما در این پروژه میخواهیم یک مجوز سفارشی ایجاد کنیم که اجازه ی ویرایش یا حذف یک پست رو فقط به نویسنده اش بده و مابقی کاربر ها فقط اجازه ی مشاهده اون پست رو داشته باشن. بدین منظور یک فایل جدید با نام: " permissions.py " در دایرکتوری posts ایجاد میکنیم و کد های زیر رو داخلش مینویسیم:
Code
# posts/permissions.py
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
# کاربرانی که احراز هویت شده باشند، امکان خواندن لیست داده ها را دارند
if request.user.is_authenticated:
return True
return False
def has_object_permission(self, request, view, obj):
# همه ی کاربران امکان خواندن پست مورد نظر را دارند
if request.method in permissions.SAFE_METHODS:
return True
# امکان ویرایش یا حذف پست فقط برای نویسنده ی آن مقدور است
return obj.author == request.user
در ابتدای فایل میایم و permissions رو فراخوانی میکنیم و بعدش یک کلاس با نام " IsAuthorOrReadOnly " تعریف میکنیم که حاوی دو متد هستش. متد اول (has_permission) امکان دسترسی به محتوای درخواستی رو فقط برای کاربرانی که لاگین کرده باشند میده. متد دوم (has_object_permission) امکان مشاهده و خواندن محتوای درخواستی رو به همه کاربرها میده ولی اجازه ی ویرایش یا حذفش رو فقط به مالک یا همون نویسنده ی محتوا میده.
***: در قطعه کد بالا ، SAFE_METHODS اشاره داره به متد های GET ، HEAD و OPTIONS .
در ادامه کار میریم سراغ فایل posts/views.py و مجوز جدیدی که ایجاد کردیم رو در ابتدای فایل فراخوانی میکنیم و سپس این مجوز رو با استفاده از فیلدِ permission_classes ، به کلاسهای PostList و PostDetail اضافه میکنیم.
Code
# posts/views.py
from rest_framework import generics, permissions
from .models import Post
from .permissions import IsAuthorOrReadOnly # new
from .serializers import PostSerializer
class PostList(generics.ListCreateAPIView):
permission_classes = (IsAuthorOrReadOnly,) # new
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsAuthorOrReadOnly,) # new
queryset = Post.objects.all()
serializer_class = PostSerializer
خب کار تمومه. برای اینکه خروجی مورد انتظارمون رو در عمل ببینیم، با استفاده از حسابِ testuser یک پست جدید ایجاد میکنیم و مطمئن میشیم که این کاربر به عنوان نویسنده ی پست مورد نظر میتونه بهش دسترسی داشته باشه. مشخصه که ما اگر از حساب superuser برای اینکار استفاده کنیم نمیتونیم از خروجی مورد نظر اطمینان داشته باشیم، چون اَدمین بصورت پیشفرض بدون نیاز به هیچگونه مجوزی به همه ی محتوای حاضر دسترسی داره و میتونه هر کاری بکنه.
در مرورگر به آدرس: " /http://127.0.0.1:8000/admin " برید و وارد حساب superuser خودتون بشید و سپس یک پست جدید ایجاد کنید و testuser رو بعنوان نویسنده ی این پست جدید انتخاب کنید. (یادتون نره که روی دکمه ی Save کلیک کنید.)
بعدش به آدرس: " /http://127.0.0.1:8000/api/v1/2 " برید و با استفاده از لینک " Log Out " در منوی بالا-راست صفحه، از حساب superuser خارج بشید و این دفعه با استفاده از حساب testuser وارد بشید.
این همون خروجی مورد انتظار ماست که میتونیم پست مورد نظر رو به عنوان testuser یا نویسنده اش، ویرایش یا حذف کنیم. اما اگر به پست اول در آدرس: " /http://127.0.0.1:8000/api/v1/1 " سر بزنید، میبینید که فقط امکان مشاهده پست رو داریم و نمیتونیم ویرایش یا حذفش کنیم چون توسط یه کاربر دیگه (superuser) ایجاد شده.
حالا اگه میخواهید که کاملا از بابت همه چیز خیالتون راحت بشه میتونید از حساب testuser فعلی هم خارج بشید و سعی کنید که به اِندپوینت لیست پست ها یا هر کدوم از پست ها سربزنید تا مطمئن بشید که کاربرانی که لاگین نکرده باشند، نمیتونن به چیزی هم دسترسی داشته باشن.
Git
نهایتا هم پیشرفت خودمون در این بخش از پروژه رو ثبت میکنیم:
Shell
(blogapi-JvRm-7-t) > git status
(blogapi-JvRm-7-t) > git add -A
(blogapi-JvRm-7-t) > git commit -m "add permissions"
جمع بندی
دست و پا کردن مجوز های دسترسی مناسب و سنجیده، بخش مهمی از هر API هستش. ما در این بخش با انواع این مجوز ها آشنا شدیم و یک مجوز متناسب با نیاز های پروژه خودمون رو پیاده کردیم.
در بخش بعدی راجع به احراز هویت کاربران صحبت میکنیم.