نکات کلیدی
- Spring Security یک چارچوب Java/Jakarta EE است که احراز هویت، مجوزدهی و سایر قابلیتهای امنیتی را برای برنامههای سازمانی فراهم میکند.
- توسعهدهندگان میتوانند پیکربندیهای جامع را در رابط SecurityFilterChain مربوط به Spring Security پیادهسازی کنند تا CORS، محافظتهای CSRF، و فیلترهای احراز هویت را مدیریت کنند، در حالی که اجازهٔ دسترسی به endpointهای مشخصی مانند sign-up و login را میدهند.
- توکنهای دسترسی و نوسازی میتوانند بهصورت راهبردی استفاده شوند تا بین دغدغههای امنیتی و راحتی کاربر تعادل برقرار شود، ریسکهای بهخطر افتادن توکن را کمینه کند و در عین حال تجربهٔ کاربر را بهبود دهد.
- Axios میتواند در برنامههای سمت کلاینت برای مدیریت کارآمد درخواستهای مبتنی بر توکن استفاده شود، با مداخله کنندهایی که درج توکن و سناریوهای نوسازی را مدیریت میکنند و تعاملات کاربر را مقاوم و بدون وقفه تضمین میکنند.
- نمودارهای جریان میتوانند برای درک بهتر فراخوانیهای API که Spring Security در پشتصحنه هماهنگ میکند استفاده شوند.
در این مقاله، یک راهحل برای ثبتنام و احراز هویت یک کاربر از طریق یک برنامهٔ JavaScript سمت کلاینت با استفاده از زیرساخت Spring Security، و توکنهای access و refresh را بررسی میکنیم.
نمونههای پایهٔ زیادی برای استفاده از Spring Security وجود دارد، بنابراین هدف این مقاله این است که فرایند ممکن را با جزئیات کمی بیشتر و با استفاده از نمودارهای جریان توصیف کند.
میتوانید کد منبع این نمونه را در این مخزن GitHub پیدا کنید.
نکته: این مقاله روی سناریوهای موفقِ پایه تمرکز خواهد کرد. مدیریت خطا و مدیریت استثنا در اینجا حذف شدهاند.
اصطلاحات
- Authentication فرایند تأیید هویت یک کاربر است.
- Authorization فرایند تعیین این است که یک کاربر اجازه دارد به چه منابع یا اقدامهایی دسترسی داشته باشد.
- Access Token یک موجودیت دادهای است که شامل اطلاعات لازم برای شناسایی یک کاربر یا اعطای دسترسی به منابع محدودشده است.
- Refresh Token یک اعتبارنامه است که به یک برنامهٔ کلاینت اجازه میدهد بدون اینکه کاربر مجبور شود دوباره وارد شود، توکنهای دسترسی جدید دریافت کند. مفهوم refresh token شامل یک مصالحه (trade-off) بین امنیت و راحتی کاربر است. در حالی که نگه داشتن یک access token با عمر طولانی ریسک بهخطر افتادن را ایجاد میکند، مجبور کردن کاربر به ورود مکرر تجربهٔ کاربر را تضعیف میکند. Refresh tokenها این مسئله را با این کارها حل میکنند:
اجازه دادن به برنامهٔ کلاینت برای دریافت یک جفت توکن جدید پس از منقضی شدن access token، بدون اینکه کاربر مجبور شود دوباره وارد شود.
کاهش بازهٔ زمانیای که طی آن access token در معرض بهخطر افتادن قرار دارد.
فهرست فرایندهای پایه و پیکربندی Spring Security
این سیستم از سناریوهای اساسی زیر پشتیبانی میکند:
- ثبت نام کاربر.
- احراز هویت و مجوز کاربر از طریق فرم ورود به سیستم، و سپس هدایت به صفحه کاربر.
- فرآیند کسب و کار – درخواست تعداد کاربران ثبت نام شده.
- بهروزرسانی توکن.
پیکربندی کلی Spring Security میتواند در متد filterChain() که در کلاس SecurityConfiguration تعریف شده است انجام شود:

بیایید هر سناریو را جداگانه تجزیه کنیم.
ثبتنام کاربر
وقتی یک کاربر فرم ثبتنام را با همهٔ فیلدهای لازم پر میکند و درخواست را ارسال میکند، مراحل زیر همانطور که در Figure 1 نشان داده شده است رخ میدهد:

برای اجازه دادن به دسترسی به endpointِ /signup و مجاز کردن درخواستها برای دور زدن الزام پیشفرض احراز هویتِ Spring Security، باید Spring Security را طوری پیکربندی کنید که دسترسی بدون احراز هویت را برای این endpoint مشخص مجاز کند. این کار میتواند با تغییر پیکربندی امنیتی برای خارج کردن endpointِ /signup از الزام احراز هویت انجام شود.
در اینجا نحوهٔ پیکربندی Spring Security برای اجازه دادن به دسترسی به endpointِ /signup با استفاده از این بخش از متد filterChain() که پیشتر ذکر شد و در کلاس SecurityConfiguration تعریف شده است آورده شده است:

نکتهٔ مهم بعدی این است که پیکربندی شامل یک token filter است، که همهٔ درخواستها را رهگیری (intercept) میکند و توکن داخل آنها را همانطور که در این بخش از متد filterChain() نشان داده شده است بررسی میکند:

برای خارج کردن این بررسی برای درخواست ثبتنام، باید سازوکار تشخیص مسیرهایی را که این فیلتر با آنها کار میکند، هنگام ساختن token filter مشخص کنید. بیایید به متد buildTokenAuthenticationFilter() که در کلاس SecurityConfiguration تعریف شده است نگاه کنیم:

اینجا ما از کلاس SkipPathRequestMatcher (همانطور که در زیر نشان داده شده است) استفاده میکنیم که مسیرهایی را که در پارامتر pathsToSkip مشخص شدهاند از مسیرهای فیلتر حذف میکند (در مورد ما، ما SIGNUP_ENTRY_POINT را به این آرایه اضافه کردیم).

احراز هویت و مجوزدهی کاربر از طریق فرم ورود
به محض اینکه درخواست با موفقیت از token filter عبور کند، مطابق Figure 2 برای رسیدگی به business controller ارسال میشود:

-
کلاینت نام کاربری و گذرواژه را به endpointِ سرور، /login، ارسال میکند.
-
برای اینکه LoginAuthenticationFilter درخواست را رهگیری کند، باید Spring Security را مطابق آن پیکربندی کنید:

این فیلتر را تعریف کنید و URI را برای فیلتر کردن درخواستها با استفاده از متد buildLoginProcessingFilter() که در کلاس SecurityConfiguration تعریف شده است مشخص کنید:
توجه کنید که علاوه بر URI، هنگام ساختن فیلتر، ما همچنین handlerهایی برای مجوزدهی موفق و ناموفق، و نیز یک Authentication Manager مشخص میکنیم. جزئیات مربوط به آنها در ادامه بررسی خواهد شد.
این URI را با استفاده از متد buildTokenAuthenticationFilter() که در کلاس SecurityConfiguration تعریف شده است به فهرست استثناها (exclusions) برای token filter اضافه کنید:

فیلتر ساختهشده را از طریق متد filterChain() به پیکربندی اضافه کنید:

در کلاس LoginAuthenticationFilter، ما دو متد را override میکنیم که Spring در طول اجرای فیلتر فراخوانی میکند. متد اول attemptAuthentication() است، جایی که ما یک درخواست احراز هویت را به متد AuthenticationManager که هنگام ساختن فیلتر ارائه کردیم آغاز میکنیم. با این حال، خودِ manager احراز هویت انجام نمیدهد؛ بلکه بهعنوان یک container برای providerهایی عمل میکند که این وظیفه را انجام میدهند. رابط AuthenticationManager مسئولیت پیدا کردن provider مناسب و ارسال درخواست به آن را بر عهده دارد. اینجا نحوهٔ ساختن manager و ثبت providerها آورده شده است:

سپس، این manager بهعنوان پارامتر برای هر فیلترِ ساختهشده مشخص میشود.
-
برای اینکه AuthenticationManager بتواند provider موردنیاز را پیدا کند (در مورد ما، LoginAuthenticationProvider)، لازم است داخل خود provider مشخص شود که از چه نوعی پشتیبانی میکند، همانطور که در متد supports() در زیر نشان داده شده است:

در مثال ما، مشخص میکنیم که provider از کلاس UsernamePasswordAuthenticationToken پشتیبانی میکند. وقتی ما یک شیء از نوع UsernamePasswordAuthenticationToken در فیلتر میسازیم و آن را به AuthenticationManager میدهیم، این manager میتواند با توجه به نوع شیء، provider موردنیاز را بهدرستی پیدا کند، با استفاده از متد attemptAuthentication() که در کلاس LoginAuthenticationFilter تعریف شده است:

-
بعد از اینکه AuthenticationManager provider موردنیاز را پیدا کرد، متد authenticate() را فراخوانی میکند و provider بهصورت مستقیم اعتبارسنجی (validation) ورود و گذرواژهٔ کاربر را انجام میدهد. سپس نتیجه به فیلتر بازگردانده میشود.
-
متد دوم که در فیلتر override میکنیم successfulAuthentication() است، که Spring پس از احراز هویت موفق آن را فراخوانی میکند. نقش رسیدگی به احراز هویت موفق بر عهدهٔ رابط Spring Security AuthenticationSuccessHandler است، که ما هنگام ساختن فیلتر آن را مشخص کردیم (همانطور که بالاتر گفته شد). این handler یک متد override شده دارد، onAuthenticationSuccess()، که در آن معمولاً توکنهای تولیدشده را ثبت میکنیم و کد پاسخ موفق را برای درخواست تنظیم میکنیم.

سپس، زیرساخت Spring، با داشتن یک پاسخ موفق در اختیار، آن را به کلاینت ارسال میکند.
فرایند کسبوکاری – درخواست تعداد کاربران ثبتنامشده
در مثال ما، برای درخواست کسبوکاری، یک درخواست برای بازیابی تعداد کاربران داخل پایگاه داده را در نظر میگیریم. رفتار مورد انتظار این است که برای هر درخواستی که توسط یک کاربر واردشده (logged-in user) آغاز میشود، ما توکن را بررسی کنیم. فرایند بررسی توکن توسط TokenAuthenticationFilter آغاز میشود و سپس، با دنبال کردن یک فرایند مشابه آنچه بالاتر توصیف شد، درخواست به TokenAuthenticationProvider واگذار (delegated) میشود. بعد از بررسی موفق، فیلتر درخواست را به زنجیرهٔ استاندارد فیلترهای برنامهٔ وب هدایت میکند، و در نتیجه، درخواست به business controller یعنی AuthController میرسد، همانطور که در Figure 3 نشان داده شده است.

-
کلاینت یک درخواست به endpointِ سرور، /users/count، همراه با توکن ارسال میکند.
-
برای اینکه TokenAuthenticationFilter بتواند درخواست را رهگیری کند، باید آن را در پیکربندی Spring Security تنظیم کنید:
این فیلتر را بسازید (ما این فیلتر را در فرایندهای بالا دیدیم) و URI را برای فیلتر کردن درخواستها مشخص کنید (در این مورد، همهٔ درخواستها به جز آنهایی که در کلاس SkipPathRequestMatcher استثنا شدهاند)، باید آن را در پیکربندی Spring Security با متد buildTokenAuthenticationFilter() همانطور که در زیر نشان داده شده است تنظیم کنید:

مثل فیلتر قبلی، ما AuthenticationManager را مشخص میکنیم، که برای پیدا کردن provider فراخوانی خواهد شد.
فیلتر ساختهشده را با متد filterChain() ما به پیکربندی اضافه کنید:

برای اینکه AuthenticationManager provider موردنیاز را پیدا کند، ما از متد authenticationManager() استفاده میکنیم:

در خود provider، نوعی را که درخواستها باید بر اساس آن فیلتر شوند از طریق متد supports() که در کلاس TokenAuthenticationProvider تعریف شده است مشخص کنید:

در نتیجه، فیلتر باید یک شیء JwtAuthenticationToken بسازد. سپس AuthenticationManager provider مناسب را بر اساس نوع آن پیدا میکند و شیء را برای احراز هویت با استفاده از attemptAuthentication() که در TokenAuthenticationFilter تعریف شده است ارسال میکند.
-
پس از احراز هویت موفق، متد successfulAuthentication() درخواست اصلی را به زنجیرهٔ فیلترهای استاندارد forward میکند، که در نهایت به business controller یعنی AuthController میرسد.
فرایند refresh کردن توکن مشابه فرایند login است:

کلاینت یک درخواست refresh توکن به endpointِ /refreshToken ارسال میکند.
- درخواست توسط RefreshTokenAuthenticationFilter رهگیری میشود چون URI مشخصِ endpoint در فهرست URIهای مجاز برای فیلتر قرار دارد.
- فیلتر با استفاده از متد attemptAuthentication() تلاش برای احراز هویت میکند، با دسترسی به AuthenticationManager، که به نوبهٔ خود RefreshTokenAuthenticationProvider را فراخوانی میکند.
- همانطور که در دو مثال بالا توصیف شد، این provider انتخاب میشود چون از یک نوع مشخص پشتیبانی میکند، که همان شیئی است که ما در فیلتر میسازیم – RefreshJwtAuthenticationToken:

پس از احراز هویت موفق، متد successAuthentication() همان handler یعنی LoginAuthenticationSuccessHandler را مانند فرایند login فراخوانی میکند، که جفت توکن تولیدشده را در پاسخ ثبت میکند.
شرح فرایند در سمت کلاینت
به تصویر کشیدن فرایند در سمت برنامهٔ JavaScript با استفاده از یک نمودار جریان، بهخاطر شاخهدار شدن فرایند بسته به پاسخ سرور، نسبتاً دستوپاگیر به نظر میرسد. بنابراین، بیایید مستقیم روی کد تمرکز کنیم، که مختصر است، و قدمبهقدم توضیح دهیم چه اتفاقی در آن میافتد. فایل apiClient.js را در نظر بگیرید:

- ما از کتابخانهٔ Axios برای ارسال درخواستها به سرور استفاده میکنیم.
- ما یک request interceptor در Axios ثبت میکنیم، که همهٔ درخواستها را رهگیری میکند و یک توکن به آنها اضافه میکند (با استفاده از متد authHeader()).
- ما یک response interceptor در Axios ثبت میکنیم، که همهٔ پاسخها را رهگیری میکند و منطق زیر را اجرا میکند: اگر پاسخ ناموفق باشد، کد وضعیت (status code) را بررسی میکنیم:اگر
- پاسخ شامل کد وضعیت ۴۰۱ باشد (برای مثال، در حالت توکن نامعتبر یا نبود توکن)، تمام اطلاعات مربوط به توکنهای موجود را حذف میکنیم و به صفحهٔ login هدایت (redirect) میکنیم.
- اگر پاسخ شامل کد انقضای توکن باشد (این کد توسط سرور هنگام اعتبارسنجی توکن در TokenAuthenticationProvider و RefreshTokenAuthenticationProvider تولید میشود)، علاوه بر
- این بررسی میکنیم آیا درخواست اصلی یک درخواست refresh توکن بوده است یا نه:
- اگر درخواست اصلی یک درخواست کسبوکاری معمولی بوده باشد، پیام انقضای توکن نشان میدهد access token منقضی شده است. برای refresh کردن access token، یک درخواست refresh توکن همراه با refreshToken ارسال میکنیم. سپس جفت توکن جدید را از پاسخ ذخیره میکنیم و درخواست کسبوکاری اصلی را با توکن بهروزشده دوباره تکرار میکنیم.
- اگر درخواست اصلی یک درخواست refresh توکن بوده باشد، پیام انقضای توکن نشان میدهد refreshToken هم منقضی شده است. این یعنی کاربر باید دوباره login کند. بنابراین، تمام اطلاعات مربوط به توکنهای موجود را حذف میکنیم و به صفحهٔ login هدایت میکنیم.
- اگر پاسخ موفق باشد، آن را به کلاینت forward میکنیم.
نتیجهگیری
در این مثال، چندین فرایند کلیدی کار با Spring Security و توکنها را با جزئیات و با استفاده از نمودارهای جریان بررسی کردیم. خارج از دامنهٔ این مقاله، مدیریت استثنا (exception handling) و OAuth2 قرار دارند، که آنها را جداگانه در مقالههای دیگر پوشش خواهیم داد.
