یک ماه پیش لازم شد اپ انگولار شرکت رو به identityServer وصل کنم تا کاربرها بتونن شماره موبایلشون رو بزنن، بعد یک رمز otp وارد کنند و وارد پروفایل یا قسمت های شخصی خودشون بشن. صفحه لاگین و ورود با otp به صورت custom شده سمت سرور پیاده سازی شده بود. من باید این صفحه customize شده رو داخل یک iframe نمایش میدادم و دخالتی در لاگین کاربر نداشتم تا زمانی که خبردار بشم کاربر لاگین شده.
شروع کردم به جستجو و سرچ . سه کتابخانه مختلف پیشنهادی رو تست کردم و در نهایت کتابخونه oids-client-ts رو انتخاب کردم. به نظر من عملکرد پایدارتر و در عین حال راحت تری نسبت به بقیه کتابخانه ها داره. بریم مرحله به مرحله کار رو ببریم جلو
توجه: اگر فقط میخواید پیاده سازی کنین و براتون خیلی جزییات مهم نیست، از این بخش مقاله میتونید بگذرید و به بخش بعدی برید.
هدف این بخش، توضیح سه تا مطلب اصلی در کتابخانه oidc-client-ts برای درک بهتر مطالب بعدیه.
کلاس User
این کلاس شامل همه اطلاعاتیه که از سرویس دهنده IdentityService در مورد کاربر گرفته میشه:
کلاس UserManager
این کلاس یک API سطح بالا برای ارتباط با سرویس دهنده (مدلی از IdentityService) رو پیاده سازی کرده.
کلاس UserManager امکانات زیر رو در اختیار ما قرار میده:
برای ساخت یک instance از UserManager لازمه که تنظیمات مربوط به سرویس دهنده IdentityService و سایر تنظیمات رو به constructor این کلاس پاس بدیم. اما ... این تنظیمات چی هستند؟
تنظیمات کتابخانه
این کتابخانه یک interface تعریف کرده بنام OidcClientSettings که شامل تنظیمات اصلی ارتباط با سرویس دهنده IdentityService است. همینطور یک interface بنام UserManagerSettings از روی interface اول تعریف کرده و تنظیمات مربوط به امکانات خودش رو اضافه کرده به تنظیمات اصلی.
مهمترین آیتم های موجود در این تنظیمات عبارتند از:
همینطور من یک آیتم دیگه رو هم استفاده کردم بنام popup_post_logout_redirect_uri که از نوع آیتم هاییست که مربوط به Oidc نیست بلکه از امکانات خود کتابخانه هست و در UserManagerSettings تعریف شده.
بریم سراغ پیاده سازی کتابخانه در برنامه:
برای نصب کتابخونه کافیه دستور زیر رو اجرا کنیم و بشینیم فرآیند نصبش رو نگاه کنیم:
$ npm install oidc-client-ts
یک فایل auth.service.ts درست میکنیم به همراه کلاس AuthService تا کتابخانه اضافه شدن رو مستقیم داخل اپ نبریم و هر موقع لازم بود بتونیم تغییرش بدیم یا جابجاش کنیم:
$ ng g s services/auth
من برای سرویس AuthService رفتارهای زیر رو در نظر گرفتم:
برای پیاده سازی سرویس به کلاس های User و UserManager از کتابخانه نیاز داریم:
ابتدا یک متغیر بنام userManager به سرویس اضافه میکنیم که قرار یک instance از کلاس UserManager رو نگه داره.
سپس در تابع constructor سرویس، تنظیمات IdentityServer رو به constructor کلاس UserManager پاس میدیم و یک instance میسازیم و نگهش میداریم:
در ادامه constructor ، تابع UserManager.getUser رو صدا کردم تا مشخص بشه زمانی که access token دریافت شد، چه کارهایی با access token انجام بدم. این کد رو هر جایی از برنامه تون که فکر میکنید لازمه اضافه کنید:
فرایند login:
با فراخوانی تابع signinPopup میشه لاگین مدل popup انجام داد که من در پروژه به همین روش لاگین رو پیاده سازی کردم. درواقع از صفحه ای استفاده کردم که در سرویس دهنده پیاده سازی شده بود.برای لاگین مدل popup میتوان یک div popup ساخت و داخل آن یک iframe به همراه یک id (مثلا login-iframe) تعبیه کرد. سپس به کتابخانه oidc-client-ts، شناسه iframe را ارسال کرد تا صفحه لاگین سرویس دهنده در iframe باز شود:
همچنین میتوان پارامترهای اضافه ای از سمت کلاینت برای سرویس دهنده ارسال کرد تا سرویس دهنده به کمک آنها صفحه ای custom پیاده سازی کند. این بستگی به نحوه پیاده سازی سرویس دهنده دارد.
اما فرایند اینجا تموم نشده. وقتی کار login تموم میشه ، از صفحه لاگین ریدایرکت میکنه به صفحه دیگه ای که اینجا مثلا بهش میگیم redirected-from-login . من این صفحه رو یک کامپوننت خالی در نظر گرفتم. ولی بعد از اینکه ریدایرکت انجام شد، در این صفحه باید تابع signinCallback رو صدا بزنیم وگرنه اطلاعات کاربر درست دست ما نمیرسه.
این مشکل نزدیک به چهار ساعت از وقت من رو گرفت تا متوجه ش شدم:
میتونیم متغیر userManager در AuthService رو private کنیم و یک تابع signinCallback برای AuthService درنظر بگیریم که همین کار رو خودش انجام بده. این روش درست تره ولی من این کار رو نکردم! :)
فرآیند renewToken:
برای افزودن فرایند renewToken به AuthService کافیه این کد رو اضافه کنیم:
فرآیند logout:
برای فرآند logout میتونیم یک iframe کاملا پنهان و دور از نظر کاربر در صفحه تعریف کنیم و نام و شناسه براش تعریف کنیم (مثلا logout-iframe):
سپس فرآیند logout مدل popup رو با فراخوانی کد زیر انجام بدیم:
و فرآیند logout اتفاق می افتد.
مشکل بزرگ در آینده
کتابخانه iodc-client-ts یک مشکل بزرگ دارد که تا این لحظه که من این مقاله رو مینویسم حل نشده. این کتابخانه اطلاعات کاربر رو در localStorage ذخیره میکنه و نام اطلاعاتی که ذخیره میکنه با "oidc." شروع میشه و در ادامه یک hash بهش اضافه میشه. زمانی که logout میکنیم این اطلاعات پاک نمیشه. هر بار که دوباره کاربر میخواهد به سرور وصل شود و لاگین کند اطلاعات جدیدی ذخیره میشود با پیشوند oidc. و hash جدید.
این فرآیند ممکن است به مرور حافظه localStorage رو پر کنه و برنامه از کار بیفته. البته این ایراد گزارش شده ولی هنوز حل نشده. برای همین باید به فکر خالی کردن این اطلاعات بود. تیکه کد زیر رو من از خودم نوشتم و بعد از صدا زدن singoutPopup گذاشتم. این راه حل کار میکنه ولی شاید راه بهتری هم باشه.
در مجموع اتصال به سرویس IdentityServer برای احزار هویت کاربر بسیار ساده، استاندارد و راحته. حتی وصل شدن با ارسال Username و Password هم از این خیلی خیلی ساده تره. امیدوارم این مقاله براتون مفید باشه :)