اگر شما هم مثل من زیاد با django سروکله میزنید، با فهمیدن روند کار بخشهای مختلف اون، درک بهتری از رفتار اون خواهیم داشت، که اون هم باعث میشه در مواقع مختلف(از خطایابی یا توسعه ویژگی جدید) بتونیم تصمیمات بهتری رو بگیریم. یکی از بخشها مدیریت sessionهاست که به صورت پیشفرض هم تو پروژههای جنگویی وجود داره و جدولش تو پایگاه داده ساخته میشه. تو این پست میخوایم بفهمیم که داخل این جدول چه چیزهایی ذخیره میشه و جنگو ازش چطوری استفاده میکنه.
جنگو تو نسخه ۳ خودش تو این بخش یک سری تغییراتی داشته، ولی از اونجایی که هنوز بخش خوبی از پروژههای جنگویی با نسخه ۲ اون هستش، به خاطر همین به هر دو نسخه پرداخته میشه(البته من نسخه ۲٫۲ و ۳٫۲ جنگو رو بررسی کردم و از بقیه نسخهها خبر ندارم).
برای شروع با پایگاه داده و یک جدول طبیعتا آشنایی با ساختار اون بهترین گزینه روی میزه(یادش بخیر یه زمانی هم با "همه گزینهها روی میزه" تهدید میشدیم. البته خوبیش این بود فقط تهدید بود D:). جدول session سه تا ستون به اسمهای session_key, session_data, expire_date رو شامل میشه. نوع دادههای این سه تا ستون رو میتونید تو عکس پایین ببنید(اون NN یعنی Not Null):
و البته نمونهای از دادهای که تو اون ذخیره شده:
خب از اونجایی که session_key و expire_date مشخصه برای چی هستند، توضیحی درموردشون نمیدیم و میریم سراغ ستون session_data تا ببنیم که چه اطلاعاتی رو تو خودش ذخیره کرده.
تفاوت نسخه ۲ با نسخه ۳ از دادهای که تو این ستون ذخیره میشه. در واقع اطلاعاتی که ذخیره میشه یکیه ولی نحوه پیشپردازش این اطلاعات متفاوته که باعث تولید داده متفاوت برای هر نسخه میشه.
اول از همه بریم ببینیم که تو این ستون چی ذخیره شده:
خب مشخصه که چیز قابل فهمی برای ما نیست. اما از اون جایی که ما خیلی کنجکاویم، به این راحتیها بیخیال ماجرا نمیشیم و میریم که یک نگاهی به کد منبع(source code) جنگو بندازیم. سوال اولی که مطرح میشه اینکه که خب برادر من جنگو دو خط کد نیست که جای کد جنگو رو نیگا کنیم؟ جواب سوال هم اینکه خب اصولا وقتی داریم درمورد session حرف میزنیم باید ماژول session رو نگاه کنیم دیگه! برای همین میریم تو مسیر زیر و یک نگاهی به کلاس SessionStore میندازیم:
from django.contrib.sessions.backends.db import SessionStore
و با همین صحنه ای مواجه میشیم:
تو این کلاس اون بخشی که تو عکس highlight شده، جایی که میره داده رو از پایگاه داده میخونه و به زبون آدمیزاد تبدیلش میکنه(البته ممکنه تو ادامه دوباره دستکاری بشه). گرفتن رکورد مرتبط از پایگاه داده که هیچی مشخصه(خط ۴۳)، پس بریم سراغ تابع decode و پیاده سازی اون رو ببینیم(خط ۴۴). این تابع توی کلاس پدر این کلاس(یعنی کلاس SessionBase) پیادهسازی شده و به این شکله:
def decode(self, session_data): try: return signing.loads(session_data, salt=self.key_salt, serializer=self.serializer) # RemovedInDjango40Warning: when the deprecation ends, handle here # exceptions similar to what _legacy_decode() does now. except signing.BadSignature: try: # Return an empty session if data is not in the pre-Django 3.1 # format. return self._legacy_decode(session_data) except Exception: logger = logging.getLogger('django.security.SuspiciousSession') logger.warning('Session data corrupted') return {} except Exception: return self._legacy_decode(session_data)
ما به خط سوم رو بهش علاقهمندیم یعنی این خط:
signing.loads(session_data, salt=self.key_salt, serializer=self.serializer)
اگر هم براتون سواله که signing چیه؟ خدمتتون عرض کنم که یه ماژول جنگو که از مسیر زیر میتونید بهش بهش دسترسی داشته باشید:
from django.core import signing
و تابع loads اون هم به این شکله:
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None): """ Reverse of dumps(), raise BadSignature if signature fails. The serializer is expected to accept a bytestring. """ return TimestampSigner(key, salt=salt).unsign_object(s, serializer=serializer, max_age=max_age)
خب اینم که داره unsign_object رو صدا میزنه(دنبال کردن این فراخوانیهای تو در تو، در مهندسی معکوس کد یه چیز کاملا عادیه)! این تابع هم به این شکله:
def unsign_object(self, signed_obj, serializer=JSONSerializer, **kwargs): # Signer.unsign() returns str but base64 and zlib compression operate # on bytes. base64d = self.unsign(signed_obj, **kwargs).encode() decompress = base64d[:1] == b'.' if decompress: # It's compressed; uncompress it first. base64d = base64d[1:] data = b64_decode(base64d) if decompress: data = zlib.decompress(data) return serializer().loads(data)
و بله تبریک میگم بالاخره رسیدیم به اونجایی که باید میرسیدیم.خب مشخصا این تابع داره کار اصلی رو انجام میده و متن ورودی را از اون حالت غیر قابل فهم به یک حالت دیگه تبدیل میکنه به خاطر همین این تابع رو یکم با دقت نگاه کنید چون در ادامه هی بهش برمیگردیم. اول کار میاد اون مقدار رو از حالت sign خارج میکنه که این کار به این صورته:
def unsign(self, signed_value): if self.sep not in signed_value: raise BadSignature('No "%s" found in value' % self.sep) value, sig = signed_value.rsplit(self.sep, 1) if ( constant_time_compare(sig, self.signature(value)) or ( self.legacy_algorithm and constant_time_compare(sig, self._legacy_signature(value)) ) ): return value raise BadSignature('Signature "%s" does not match' % sig)
این تابع کار خاصی نمیکنه متن رو میگیره اون رو rsplite میکنه با دو نقطه(منظور `:` ) از اولین دو نقطه در سمت راست(یا آخرین دو نقطه از سمت چپ) جدا میکنه تا آخر رشته و اون بخش جدا شده میشه بخشیه که برای تایید امضا بهش نیاز داریم و بعد از تایید امضا هم بهش نیازی نیست. پس متن ورودی الان ازش آخرین دو نقطه به بعد جدا شد و برگشت به تایع قبلی مون(روند تایید امضا رو دیگه نرفتیم ولی اونم میشد بریم ببینیم چطوریاست).
بعد از تایید صحتسنجی نوبت به بررسی این میرسه که آیا متن فشرده سازی شده یا نه؟ این تشخیص این مورد هم جنگو خیلی ساده اومده بررسی میکنه اگر اولین کاراکتر این متن برابر .(همون دات یا نقطه خودمون) بود یعنی بله عملیات فشرده سازی روی اون انجام شده. از اینجا میفهمیم که جنگو موقع ذخیره کردن اگر فشرده سازی رو انجام داده باشه یک dot به رشته اضافه میکنه(اگر خواستین تابع sign_object رو بغل همین تابع unsign_object نگاه کنید).
در ادامه اول dot از متن حذف میشه(خط ۸) و با کمک کتابخونه base64 که برای خود پایتون هستش عملیات decode انجام میشه(خط ۹). حالا اگر متن فشردهسازی شده باشه، برای برگردوندن به حالت عادی به تابع از تابع decompress تو کتابخونه zlib استفاده میشه(خط ۱۱) و بله تبریک میگم داده شما آمادهست.
و در نهایت چیزی که برمیگرده همچین چیزیه:
1a19a60d28166014f835175fbb7b0c2be11a1905: {"_auth_user_id":"1","_auth_user_backend":"django.contrib.auth.backends.ModelBackend", "_auth_user_hash":"0eee586903fb6b3a54d73d8b2e4e8f08d7b0e0b0"}
مثل جنگو ۳ اول کار بریم ببینیم که داخل ستون session_data چی ذخیره شده(عکس پایین).
دادهای که تو session_data ذخیره شده.
اینجا هم چیزی که ذخیره شده، قابل فهم نیست اما از شکل و شمایلش به نظر base64 میاد(البته واقعا هم base64 هستش). پس بیایم با همدیگه اون رو decode کنیم و ببینیم که به چی میرسیم(یکی از این دیکودرهای آنلاین میشه تست کرد):
1a19a60d28166014f835175fbb7b0c2be11a1905: {"_auth_user_id":"1","_auth_user_backend":"django.contrib.auth.backends.ModelBackend", "_auth_user_hash":"0eee586903fb6b3a54d73d8b2e4e8f08d7b0e0b0"}
حتی خودمم انتظار نداشتم به این زودی به خروجی برسیم!! D:
اگر هم بخوایم که تابع دیکود را در جنگو۲ ببینیم به همچین چیزی میرسیم:
def decode(self, session_data): encoded_data = base64.b64decode(session_data.encode('ascii')) try: # could produce ValueError if there is no ':' hash, serialized = encoded_data.split(b':', 1) expected_hash = self._hash(serialized) if not constant_time_compare(hash.decode(), expected_hash): raise SuspiciousSession("Session data corrupted") else: return self.serializer().loads(serialized) except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions. If any of # these happen, just return an empty dictionary (an empty session). if isinstance(e, SuspiciousOperation): logger = logging.getLogger('django.security.%s' % e.__class__.__name__) logger.warning(str(e)) return {}
بعد از اینکه این اطلاعات بدست اومد، به واکشی اطلاعات کاربر پرداخته میشه و کاربر به شی request اضافه میشه که در نتیجه اون ما تو جاهای مختلف میتونیم با request.user کاربر رو داشته باشیم، بدون اینکه واکشی دوباره اطلاعات نیاز باشه.
همین! امیدوارم مفید بوده باشه.
لبخند بزنین لطفا :)
منابع: