أساسيات Spring Security

المصادقة مقابل التفويض

18 دقيقة الدرس 3 من 13

المصادقة مقابل التفويض

يقوم كل نظام محمي على ركيزتين متمايزتين: المصادقة (من أنت؟) والتفويض (ما الذي يُسمح لك بفعله؟). هذان السؤالان متتاليان منطقيًا ولا يجوز الخلط بينهما أبدًا. إن الخلط بينهما هو أحد أكثر مصادر الثغرات الأمنية شيوعًا في التطبيقات الحقيقية — فقاعدة تفويض مُهيَّأة بشكل خاطئ تتيح لمستخدم مصادَق عليه الوصول إلى مورد لا يجب أن يراه تُعدّ اختراقًا للبيانات لا مجرد فشل في تسجيل الدخول.

يتولى Spring Security معالجة كلا الأمرين، لكن عبر نظامين فرعيين منفصلَين ومُحدَّدَي المعالم. يمنحك هذا الدرس الأساس المفاهيمي وواجهة Spring Security 6 العملية لكل منهما.

المصادقة — إثبات الهوية

المصادقة هي عملية التحقق من أن الكيان الرئيسي (مستخدم أو خدمة أو جهاز) هو فعلًا من يدّعي أنه إياه. الآلية الكلاسيكية هي اسم المستخدم وكلمة المرور، لكن المفهوم أوسع من ذلك: رمز JSON Web Token، وشهادة X.509 للعميل، ورمز وصول OAuth 2، وتأكيد SAML — كلها أشكال من بيانات الاعتماد التي يمكن استخدامها للمصادقة على كيان رئيسي.

في Spring Security تكون نتيجة المصادقة الناجحة كائنًا من نوع Authentication مخزَّنًا في SecurityContext. يحتوي على ثلاثة عناصر:

  • الكيان الرئيسي (Principal) — من جرى التحقق منه (عادةً كائن UserDetails).
  • بيانات الاعتماد (Credentials) — ما استُخدم لإثبات الهوية (تُمحى بعد المصادقة لأغراض أمنية).
  • الصلاحيات (Authorities) — مجموعة الأذونات الممنوحة (كائنات GrantedAuthority) المرتبطة بهذا الكيان.

يمكنك فحص الكيان الرئيسي المصادَق عليه حاليًا في أي مكان من تطبيقك:

import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; public class SecurityUtils { public static String currentUsername() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) { return "anonymous"; } Object principal = auth.getPrincipal(); if (principal instanceof UserDetails ud) { return ud.getUsername(); // مطابقة النمط مع instanceof في Java 16 } return principal.toString(); // احتياطي لـ OAuth2 / JWT } }
SecurityContext يعمل بالخيط المحلي (thread-local) افتراضيًا. يخزن Spring Security كائن Authentication في ThreadLocal، لذا يكون متاحًا تلقائيًا طوال عمر طلب الـ servlet دون الحاجة إلى تمريره كمعامل. في التطبيقات التفاعلية (WebFlux) يُخزَّن السياق في الخط الأنابيبي التفاعلي عوضًا عن ذلك — لا تفترض أبدًا دلالات thread-local هناك.

التفويض — تطبيق ما هو مسموح به

يعمل التفويض بعد المصادقة. بمجرد أن يعرف النظام هوية المُستدعي، يجب أن يقرر ما إذا كان يجوز لهذا المُستدعي تنفيذ الإجراء المطلوب على المورد المطلوب. يُعبّر Spring Security عن التفويض بصلاحيات ممنوحة (أدوار وأذونات) تُفحص مقابل قواعد تُهيّئها أنت.

ثمة مكانان رئيسيان يُطبّق فيهما Spring Security التفويض:

  1. طبقة HTTP — قواعد تُطبَّق على الطلبات الواردة قبل وصولها إلى وحدة التحكم، تُهيَّأ عبر SecurityFilterChain.
  2. طبقة الدوال — تعليقات توضيحية مثل @PreAuthorize تُطبَّق على دوال الخدمة أو وحدة التحكم، وتُطبَّق عبر AOP.

يبدو إعداد التفويض البسيط في Spring Security 6 على النحو التالي:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() // لا حاجة للمصادقة .requestMatchers("/admin/**").hasRole("ADMIN") // مصادَق + يملك ROLE_ADMIN .requestMatchers("/api/**").hasAuthority("API_READ") // إذن محدد .anyRequest().authenticated() // كل شيء آخر: مصادَق فقط ) .formLogin(form -> form .loginPage("/login") .permitAll() ); return http.build(); } }
الأدوار مقابل الصلاحيات: الدور هو صلاحية مُسمّاة تحمل البادئة ROLE_. عندما تستدعي hasRole("ADMIN") يبحث Spring Security عن صلاحية باسم ROLE_ADMIN — يُضيف البادئة تلقائيًا. عندما تستدعي hasAuthority("API_READ") يبحث عن النص الحرفي ذاته. استخدم الأدوار للوظائف العامة (ADMIN, USER, MANAGER) والصلاحيات الدقيقة للقدرات المحددة (INVOICE_WRITE, REPORT_READ).

لماذا يهم الترتيب — ولماذا يجب أن يبقيا منفصلَين

المصادقة تعمل دائمًا أولًا. إذا لم يُمكن التحقق من هوية الطلب فلا شيء للتفويض — يُرفض الطلب بـ 401 Unauthorized. فقط بعد وجود كائن Authentication صالح في SecurityContext تُفعَّل قواعد التفويض. أما رفض التفويض فيُعيد 403 Forbidden.

تحمل هاتان الرموز دلالات محددة يعتمد عليها عملاء واجهة برمجية التطبيقات:

  • 401 — "لم تُثبت هويتك. قدّم بيانات اعتماد صالحة."
  • 403 — "أعرف هويتك، لكن لا يُسمح لك بهذا."
إعادة 403 حين يجب إعادة 401 يُسرّب معلومات. إذا أعادت واجهة برمجية التطبيقات الخاصة بك 403 Forbidden لطلب غير مصادَق على نقطة نهاية محمية، فأنت تُخبر المهاجمين بأن نقطة النهاية موجودة ومحمية. أعد دائمًا 401 أولًا، واحتفظ بـ 403 للحالات التي يكون فيها المستخدم مصادَقًا لكنه غير مُخوَّل. يتعامل Spring Security مع هذا بشكل صحيح افتراضيًا — لا تتجاوز AuthenticationEntryPoint وAccessDeniedHandler إلا إذا كنت تفهم التبعات.

المصادقة والتفويض في واجهة REST API عديمة الحالة

تخزّن تطبيقات الويب التقليدية كائن Authentication في جلسة HTTP (ذات حالة). أما واجهات REST API عديمة الحالة — والخدمات المصغّرة — فتتبع مقاربة مختلفة: يُقدّم العميل رمزًا مكتفيًا بذاته (عادةً JWT) مع كل طلب، ويتحقق منه الخادم دون الرجوع إلى أي مخزن جلسات.

في هذا النموذج تتحول الركيزتان قليلًا:

  • المصادقة تحدث عند خطوة التحقق من الرمز: يُتحقق من توقيع JWT، ويُفحص انتهاء صلاحيته، وتُستخرج هوية الموضوع والادعاءات المضمّنة لإعادة بناء كائن Authentication في SecurityContext.
  • التفويض ثم يسير بشكل متطابق: تُفحص الصلاحيات المضمّنة في الرمز (أو المُجلَبة من قاعدة البيانات) مقابل القواعد.
// ادعاءات نموذجية في JWT تُستخدم للمصادقة والتفويض معًا { "sub": "alice@example.com", // المصادقة: من هذا؟ "roles": ["ROLE_USER"], // التفويض: ماذا يمكنها أن تفعل؟ "permissions": ["INVOICE_READ"], // تفويض دقيق "exp": 1718000000 // الانتهاء: جزء من صلاحية المصادقة }
مقايضة الأنظمة الموزّعة: تضمين الصلاحيات في JWT يجعل الرمز مكتفيًا بذاته فلا تحتاج الخدمة إلى استعلام قاعدة بيانات لتفويض كل طلب — ممتاز للكمون. الجانب السلبي هو أن الصلاحيات المُلغاة لا تسري حتى انتهاء صلاحية الرمز. أوقات انتهاء صلاحية قصيرة (5-15 دقيقة) مع رموز تحديث هي الحل المعتمد. الدرس العاشر من هذا البرنامج (JWT) يتناول هذا بالتفصيل.

العقد الداخلي لـ Spring Security

داخليًا يفصل Spring Security المسألتين على مستوى واجهة برمجة التطبيقات:

  • AuthenticationManager / AuthenticationProvider — مسؤولان عن المصادقة. يأخذان رمزًا غير مصادَق (مثل UsernamePasswordAuthenticationToken ببيانات اعتماد خام)، يتحققان منه، ويُعيدان كائن Authentication موثوقًا ومكتملًا.
  • AccessDecisionManager / AuthorizationManager — مسؤولان عن التفويض. يتلقيان كائن Authentication المصادَق عليه وكائنًا محميًا (طلب HTTP أو استدعاء دالة) ويقرران منح الوصول أو رفضه.

ستُهيّئ هذه الواجهات وتُوسّعها طوال هذا البرنامج التعليمي. الخلاصة المعمارية الآن: تصميم Spring Security يُطبّق الفصل بين المصادقة والتفويض على مستوى الأنواع، مما يجعل الخلط بينهما عن طريق الخطأ أصعب.

الخلاصة

تُجيب المصادقة عن سؤال "من أنت؟" وتُنتج كائن Authentication موثوقًا في SecurityContext. يُجيب التفويض عن سؤال "ما الذي يجوز لك فعله؟" ويتحقق من صلاحيات ذلك الكائن مقابل القواعد التي تُعرّفها. المصادقة دائمًا أولًا؛ فشلها يُعطي 401، أما فشل التفويض فيُعطي 403. يُطبّق Spring Security الحد الفاصل بين المسألتين على مستوى واجهة برمجة التطبيقات، سواء في سلسلة مرشّح HTTP أو في طبقة AOP للأمان على مستوى الدوال. الحفاظ على وضوح هذا النموذج الذهني سيُجنّبك أكثر فئات الأخطاء الأمنية الناجمة عن سوء الإعداد شيوعًا.