سرویس احراز هویت یکتانت | معماری

در این مقاله می‌خوام درباره‌ی سرویس احراز هویت یکتانت بنویسم. این سرویس که همزمان با تولد یکتانت ایجاد شده، در طی این چهار سال تغییرات زیادی را به خود دیده است. این تغییرات جزئی از زندگی یک نرم‌افزار است و تا یکتانت زنده است ادامه خواهد داشت.

هر سرویسی که می‌خواهد خدمتی اختصاصی به کاربرانش بدهد نیاز دارد که بتواند کاربران خود را احراز هویت کند. سرویس اکانتس در یکتانت این وظیفه را برعهده دارد. چهار سال پیش که یکتانت تازه داشت متولد می‌شد، فقط یک سرویس داشتیم که نیازمند احراز هویت کاربران بود و برای آن از مکانیزم احراز هویت نشست (Session Authentication) جنگو استفاده کردیم. با گذر زمان و با افزایش سرویس‌ها و در مسیر معماری میکروسرویس، نیاز ایجاد مکانیزم مناسبی برای احراز هویت کاربران به وجود آمد.

تجربه کاربری نباید از معماری سرویس‌ها آسیب می دید. کاربری که می‌خواست همزمان از چندین سرویس‌ تبلیغاتی یکتانت استفاده کند باید همه‌ی این سرویس‌ها را یک سرویس واحدی از طرف یکتانت می‌دید. یعنی کاربر فقط باید یک‌بار ثبت‌نام می‌کرد و یکبار وارد سیستم می‌شد و پس از آن باید می‌توانست از همه‌ی سرویس‌های یکتانت استفاده کند. اصطلاحا به چنین مکانیزمی SSO یا Single Sign On می‌گویند.

با گذر زمان و با تولد پلتفرم‌های دیگر و یکپارچه شدن‌ آنها با پلتفرم یکتانت، چالش‌های جدیدی برای ایجاد یک مکانیزم امن و راحت برای احراز هویت کاربران به وجود آمد. در این مقاله می خواهم درباره‌ی چالش‌ها و راه‌حل‌های ایجاد چنین مکانیزمی در طی این چهار سال حرف بزنم.

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


اولین نشانه‌های مقیاس

در ابتدای یکتانت، همه‌ی سرویس‌ها به شکل یکپارچه‌ (monolithic) بودند و از پایگاه‌داده مشترکی استفاده می‌کردند. همچنین احراز هویت سرویس‌ها توسط مکانیزم نشست جنگو صورت می‌گرفت.

با ایجاد سرویس تبلیغاتی بنری‌ یکتانت می‌خواستیم در مسیر میکروسرویس شدن پیش برویم و تا جای ممکن سرویس‌های مستقلی داشته باشیم. این انتخاب به این معنا بود که سرویس‌های جدید دیگر دسترسی به پایگاه‌داده‌ی اصلی که اطلاعات کاربران و نشست‌ها در آن ذخیره شده بود، نخواهند داشت. بنابراین مکانیزم احراز هویت پیش‌فرض جنگو برای ما مناسب به نظر نمی‌رسید.

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

اولین ایده استفاده از پایگاه‌داده مشترک برای کاربران و اطلاعات نشست‌ها بین همه‌ی سرویس‌ها بود. این معماری هر چند ساده و خوب به نظر می‌رسید ولی ایرادات متعددی داشت. مهمترین ایرادش این بود که واسط (interface) سطح پایینی در اختیار سرویس‌ها برای کار با داده‌های کاربران می‌داد. یعنی هر تصمیمی که منجر به تغییر پایگاه‌داده‌ی مشترک می‌شد باید همه‌ی سرویس‌ها را هم تغییر می‌دادیم. از طرفی این پایگاه‌داده‌ی مشترک گلوگاه سرویس‌ها می‌شد. یعنی در صورت بروز هر گونه مشکل برای این پایگاه‌داده‌ی مشترک، همه‌ی سرویس‌ها با اختلال مواجه می‌شدند.

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


معماری سرویس‌ واسط

استفاده از یک سرویس واسط (Proxy Service) ایده‌ای بود که ابتدا توجه‌مان را جلب کرد. ایده‌ی کلی این بود که کاربران فقط به یک سرویس واسط درخواست دهند و این سرویس واسط درخواست را به سرویس مرتبط دوباره ارسال کند. در این صورت این سرویس واسط می‌توانست با مکانیزم نشست جنگو، کاربر را احراز هویت نماید و ارتباطش با سرویس‌های دیگر با استفاده از احراز هویت توکنی (Token Authentication) صورت پذیرد.

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


این معماری چندین ایراد داشت. اولا همه‌ی ترافیک از یک سرویس واسطی عبور می‌کرد که باعث می‌شد گلوگاه ترافیک شود. یعنی در صورت بروز هرگونه مشکل در این سرویسِ واسط، همه‌ی درخواست‌های کاربران دچار مشکل می‌شدند. از طرفی تعداد درخواست‌های سرویس واسط تقریبا برابر مجموع درخواست‌های دیگر سرویس‌ها بود. این نشان‌دهنده‌ی چالش‌های مقیاس‌پذیری این سرویس بود. به خاطر چنین ایراداتی، از این معماری استفاده‌ای نشد.

معماری صدور بلیت

معماری بعدی که تا حد خوبی مشکلات معماری‌های قبلی را حل می‌کرد، معماری صدور بلیت بود که از پروتکل کربروس (Kerberos) اقتباس شده بود. در این معماری سرویسی وظیفه‌ی احراز هویت کاربران و ایجاد بلیت‌های دسترسی به سرویس‌های دیگر را داشت. کاربران برای اینکه بتوانند از سرویس خاصی استفاده کنند باید بلیت خود را به آن سرویس ارسال می‌نمودند.

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

مراحل پروتکل کربروس: ۱- ابتدا کاربر، نام‌کاربری و رمز عبور خود را به سرویس  احراز هویت ارسال می‌کند. ۲- سرویس احراز هویت در صورت معتبر بودن اطلاعات  کاربر، نشست ایجاد می‌کند. ۳- کاربر درخواست ایجاد بلیتِ دسترسی را به  سرویس صدور بلیت می‌فرستد. ۴- سرویس صدور بلیت، بلیت ایجاد شده را در  اختیار کاربر می‌گذارد. ۵- کاربر با ارائه‌ی بلیتِ دسترسی، درخواستی به  سرویس مورد نظر می‌زند. ۶- سرویس مورد نظر، در صورت معتبر بودن بلیتِ کاربر  به درخواست پاسخ می‌دهد.
مراحل پروتکل کربروس: ۱- ابتدا کاربر، نام‌کاربری و رمز عبور خود را به سرویس احراز هویت ارسال می‌کند. ۲- سرویس احراز هویت در صورت معتبر بودن اطلاعات کاربر، نشست ایجاد می‌کند. ۳- کاربر درخواست ایجاد بلیتِ دسترسی را به سرویس صدور بلیت می‌فرستد. ۴- سرویس صدور بلیت، بلیت ایجاد شده را در اختیار کاربر می‌گذارد. ۵- کاربر با ارائه‌ی بلیتِ دسترسی، درخواستی به سرویس مورد نظر می‌زند. ۶- سرویس مورد نظر، در صورت معتبر بودن بلیتِ کاربر به درخواست پاسخ می‌دهد.


برای تولید چنین توکن‌هایی از استاندارد JWT یا Json Web Tokens استفاده می‌کنیم. این استاندارد که برای ایجاد توکن‌های امن (مانند توکن دسترسی) در محیط‌های ناامن (مانند ارتباط کلاینت با سرور در یک وب اپلیکیشن) که امکان تغییر توکن توسط اشخاص دیگر وجود دارد، استفاده می‌شود. این توکن دارای سه بخش هدر، بدنه و امضا است. در بخش هدر تنظیمات کلی توکن مانند الگوریتم مورد استفاده، در بدنه داده‌هایی مانند شناسه و نام کاربر و در بخش امضا، امضای دیجیتال توکن ذخیره می‌شود.

ساختار یک توکن در استاندارد JWT: توکن سه بخش هدر، بدنه و امضا دارد. هر کدام از  این سه بخش با base64 کد می‌شوند و با کاراکتر نقطه از هم جدا می‌شوند. در  بخش هدر الگوریتم مورد استفاده برای امضا و دیگر توضیحات کلی توکن قرار  می‌گیرد. در بخش بدنه، داده‌ها به شکل JSON قرار می‌گیرند. در بخش امضا هم  امضای دیجیتال توکن توسط الگوریتم رمزنگاری که در بخش هدر تعیین شده است  برای محافظت از توکن در برابر تغییرات تعبیه می‌شود.
ساختار یک توکن در استاندارد JWT: توکن سه بخش هدر، بدنه و امضا دارد. هر کدام از این سه بخش با base64 کد می‌شوند و با کاراکتر نقطه از هم جدا می‌شوند. در بخش هدر الگوریتم مورد استفاده برای امضا و دیگر توضیحات کلی توکن قرار می‌گیرد. در بخش بدنه، داده‌ها به شکل JSON قرار می‌گیرند. در بخش امضا هم امضای دیجیتال توکن توسط الگوریتم رمزنگاری که در بخش هدر تعیین شده است برای محافظت از توکن در برابر تغییرات تعبیه می‌شود.


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

در یکتانت سرویس اکانتس وظیفه‌ی احراز هویت کاربر و تولید توکن دسترسی را دارد. ابتدا کاربر با نام کاربری و رمز عبور خود وارد سرویس اکانتس می‌شود. سپس برای اینکه کاربر به سرویس‌های تبلیغاتی یکتانت دسترسی داشته باشد، توکن دسترسی ایجاد می‌کند. حال پنلِ یکتانت می‌تواند با این توکنِ دسترسی، به سرویس‌های مختلف از طریق API درخواست بفرستد. سرویس‌ها نیز می‌توانند در صورت معتبر بودن توکن، اطلاعات کاربر را از آن استخراج کنند و به درخواست کاربر پاسخ دهند.

باز هم مقیاس و باز هم چالش

معماری که در بالا اشاره شد تا پاییز پارسال به خوبی به نیاز سرویس‌ها پاسخ می‌داد. ولی تصمیم بیزینسی گرفته شد که نیازمند مقیاس بیشتر در سرویس احراز هویت بود. پلتفرم‌های نجوا و تریبون ایجاد شده بودند و می‌خواستیم کاربران پلتفرم‌ها و فرآیند احراز هویت‌شان با سرویس اکانتسِ یکتانت یکپارچه شوند. یعنی کاربران فقط یکبار ثبت‌نام و وارد می‌شدند و پس از آن از همه‌ی پلتفرم‌های تبلیغات یکتانت، پوش نجوا و رپورتاژ تریبون و دیگر پلتفرم‌هایی که در آینده ایجاد می‌شدند می‌توانستند استفاده کنند.

مهمترین چالش مهندسی این بود که توکن دسترسی پلتفرم‌ها نمی‌توانست یک توکن واحدی باشد و سرویس اکانتس می‌بایست برای هر پلتفرم توکن دسترسی اختصاصی به آن را ایجاد کند. دلیل این مورد این بود که مشکلات امنیتی یک پلتفرم خاص نباید دیگر پلتفرم‌ها را در معرض خطر قرار دهد. یعنی اگر توکن‌های دسترسی بین همه‌ی پلتفرم‌ها یکسان بودند و پلتفرمی به خاطر مشکلات امنیتیِ خود، منجر به نشتی توکنِ دسترسی می‌شد، با استفاده از این توکن نشت شده می‌شد در همه‌ی پلتفرم‌ها تغییرات ناخواسته ایجاد کرد.

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

برای حل این مشکل، در سرویس اکانتس توکن میانی به نام refresh token ایجاد کردیم. این توکن بعد از اینکه کاربری تصمیم استفاده از پلتفرمی را می‌گرفت ایجاد می‌شد که مخصوص آن پلتفرم بود. این توکن در کوکی‌های دامنه‌ی آن پلتفرم تعبیه می‌گردید. در نتیجه پنل‌ها هنگام درخواست توکنِ دسترسی، با ارسال این توکن میانی به سرویسِ اکانتس می‌توانستند توکن دسترسی دریافت نمایند.

ابتدا کاربر با ارائه‌ی نام کاربری و رمز عبور خود به سرویس اکانتس، نشستی ایجاد  می‌کند. برای دسترسی به پلتفرمی که دامنه‌ی اختصاصی خودش را دارد باید  توکن میانی (YRT) توسط اکانتس برای آن ایجاد شود. کاربر با ارسال توکن  میانی به سرویس اکانتس می‌تواند توکن دسترسی (YAT) برای دسترسی به  سرویس‌های آن پلتفرم ایجاد نماید. در نهایت کاربر با ارائه‌ی توکن دسترسی  می‌تواند به سرویس‌های پلتفرم دسترسی پیدا کند.
ابتدا کاربر با ارائه‌ی نام کاربری و رمز عبور خود به سرویس اکانتس، نشستی ایجاد می‌کند. برای دسترسی به پلتفرمی که دامنه‌ی اختصاصی خودش را دارد باید توکن میانی (YRT) توسط اکانتس برای آن ایجاد شود. کاربر با ارسال توکن میانی به سرویس اکانتس می‌تواند توکن دسترسی (YAT) برای دسترسی به سرویس‌های آن پلتفرم ایجاد نماید. در نهایت کاربر با ارائه‌ی توکن دسترسی می‌تواند به سرویس‌های پلتفرم دسترسی پیدا کند.


چون دامنه‌ی سرویس اکانتس و پلتفرم‌ها متفاوت بود و مرورگر اجازه‌ی ذخیره‌ کردن کوکی برای دامنه‌ی دیگری را نمی‌دهد این کار توسط مکانیزم redirection صورت گرفت. هر پلتفرم باید یک API‌ فراهم می‌کرد تا کوکی میانی را که به صورت کوئری‌پارام ارسال می‌شد را در دامنه‌ی خودش ذخیره نماید تا پنل آن پلتفرم بتواند به آن دسترسی پیدا کند.جزئیات دیگری برای پیاده‌سازی این معماری چند دامنه‌ای وجود دارد که خارج از حوصله‌ی این مقاله است.

سخن آخر

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

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


اگر دوست دارید به تیم یکتانت بپیوندید به این لینک مراجعه کنید.