أساسيات Spring Security

قواعد التفويض

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

قواعد التفويض

يُجيب التحقق من الهوية (Authentication) على سؤال: من أنت؟. أما التفويض (Authorization) فيُجيب على: ما الذي يُسمح لك بفعله؟. في Spring Security 6، تُعلَن قواعد التفويض لطلبات HTTP بأسلوب سلسل (fluent) داخل SecurityFilterChain باستخدام DSL الخاص بـ authorizeHttpRequests. إتقان هذه القواعد — وفهم ترتيبها وأولويتها وتداعياتها الأمنية — يُعدّ من أعلى المهارات قيمةً يمكنك بناؤها كمطوّر Spring.

DSL الخاص بـ authorizeHttpRequests

عند تسجيل bean من نوع SecurityFilterChain تستدعي http.authorizeHttpRequests(auth -> auth...). كل استدعاء داخل الـ lambda يُضيف قاعدة مطابقة طلب. يُقيّم Spring Security هذه القواعد من الأعلى إلى الأسفل ويتوقف عند أول تطابق، لذا فإن ترتيب القواعد بالغ الأهمية.

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 securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // 1. الأصول العامة — لا تتطلب مصادقة .requestMatchers("/", "/login", "/register", "/css/**", "/js/**", "/images/**").permitAll() // 2. لوحة الإدارة — يجب أن يمتلك ROLE_ADMIN .requestMatchers("/admin/**").hasRole("ADMIN") // 3. إدارة الحساب — أي مستخدم مُصادق عليه .requestMatchers("/account/**").authenticated() // 4. الاصطياد الشامل — رفض كل ما لم يُسمح به صراحةً .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login").defaultSuccessUrl("/dashboard") ) .logout(logout -> logout.logoutSuccessUrl("/")); return http.build(); } }
الترتيب مهم — ضع القواعد المحددة دائمًا قبل القواعد الشاملة. إذا وضعت .anyRequest().authenticated() قبل قواعد permitAll()، فكل طلب — بما في ذلك صفحة تسجيل الدخول — سيستلزم مصادقة، مما يُقفل المستخدمين خارج النظام نهائيًا. معظم أخطاء التكوين الأمني في تطبيقات Spring هي أخطاء في الترتيب.

مطابقات الطلبات (Request Matchers)

تستخدم Spring Security 6 الدالة requestMatchers() بدلًا من antMatchers() و mvcMatchers() اللتين أُهملتا. تقبل أنماط المسار بأسلوب Ant أو أساليب HTTP أو مزيجًا من الاثنين.

auth // أحرف بديلة بأسلوب Ant: * تطابق مقطعًا واحدًا من المسار، ** تطابق مقاطع متعددة .requestMatchers("/api/**").hasRole("API_USER") // التقييد بأسلوب HTTP والمسار معًا .requestMatchers(HttpMethod.POST, "/articles").hasRole("EDITOR") .requestMatchers(HttpMethod.GET, "/articles/**").permitAll() // مسارات متعددة في استدعاء واحد .requestMatchers("/health", "/actuator/info").permitAll() .requestMatchers("/actuator/**").hasRole("OPS")
أزالت Spring Security 6 الدالة antMatchers(). إذا كنت تنتقل من Spring Security 5، استبدل كل استدعاء لـ antMatchers() بـ requestMatchers(). بناء جملة النمط هو نفسه، لكن الدالة الجديدة تفهم أيضًا أنماط مسارات Spring MVC تلقائيًا عندما يكون مرسّل MVC موجودًا في classpath.

الأدوار مقابل الصلاحيات

تخزّن Spring Security كلًّا من الأدوار والصلاحيات الدقيقة كسلاسل نصية على كائن Authentication. الفرق بينهما هو اتفاقية تسمية: الأدوار هي صلاحيات مسبوقة بـ ROLE_.

  • hasRole("ADMIN") — يتحقق من وجود الصلاحية ROLE_ADMIN (يُضاف البادئة تلقائيًا).
  • hasAuthority("ROLE_ADMIN") — يتحقق من النص الحرفي الذي تمدّه به بالضبط، دون إضافة أي بادئة.
  • hasAnyRole("USER", "ADMIN") — يمرر إذا امتلك الرئيسي (principal) أيًّا من الأدوار المذكورة.
  • hasAnyAuthority("read:orders", "write:orders") — مفيد لنطاقات OAuth 2 أو الصلاحيات الدقيقة.
auth // قائم على الأدوار (يُضاف البادئة ROLE_ تلقائيًا) .requestMatchers("/reports/**").hasRole("MANAGER") // قائم على الصلاحيات (تطابق النص الحرفي — مناسب لنطاقات OAuth2) .requestMatchers("/api/orders").hasAuthority("orders:read") // أدوار متعددة — يكفي أي منها .requestMatchers("/dashboard").hasAnyRole("USER", "ADMIN", "MANAGER")

تأمين أنماط URL — مثال واقعي

لنفترض تطبيق تجارة إلكترونية صغيرًا بثلاثة أنواع من المستخدمين: زوار مجهولون، وعملاء مُصادق عليهم، ومديرون. إليك كيف يبدو مجموعة قواعد كاملة في الواقع العملي:

http.authorizeHttpRequests(auth -> auth // --- وصول مجهول --- .requestMatchers(HttpMethod.GET, "/products/**").permitAll() .requestMatchers("/cart/view").permitAll() .requestMatchers("/login", "/register", "/forgot-password").permitAll() .requestMatchers("/error", "/favicon.ico").permitAll() // --- للعملاء فقط --- .requestMatchers("/cart/checkout", "/orders/**", "/account/**").hasRole("CUSTOMER") // --- للمديرين فقط --- .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers(HttpMethod.DELETE, "/products/**").hasRole("ADMIN") .requestMatchers(HttpMethod.POST, "/products/**").hasRole("ADMIN") // --- اصطياد شامل: رفض الطلبات غير المتطابقة --- .anyRequest().denyAll() )

لاحظ استخدام denyAll() كقاعدة اصطياد شاملة بدلًا من authenticated(). في واجهة برمجية (API) محددة النطاق جيدًا، هذا هو الإعداد الافتراضي الأكثر أمانًا: أي نقطة نهاية لم تُدرَج صراحةً في قائمة السماح تُرفض، فلن تصبح نقاط النهاية المُضافة حديثًا عامةً بالصدفة قبل كتابة قواعد لها.

فضّل denyAll() كقاعدة اصطياد شاملة في REST APIs، و authenticated() في تطبيقات الويب التقليدية. في خدمة REST يجب السماح صراحةً لكل مورد؛ فالاصطياد الشامل الضمني بـ authenticated() قد يكشف خلسةً نقاط نهاية نسيت حمايتها.

تعبيرات قرارات الوصول باستخدام SpEL

للشروط المعقدة التي تتجاوز التحقق من دور واحد، يمكنك استخدام لغة تعبيرات Spring (SpEL) عبر access(). هذا يفتح لك باب التحقق من عنوان IP، والتحكم بالوصول حسب وقت اليوم، أو الجمع بين أدوار متعددة بمنطق بولياني:

import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; auth // يتطلب ROLE_ADMIN وأن يأتي الطلب من الشبكة الداخلية .requestMatchers("/admin/config") .access(new WebExpressionAuthorizationManager( "hasRole('ADMIN') and hasIpAddress('10.0.0.0/8')" )) // المديرون أو المشرفون يستطيعون عرض التقارير المالية .requestMatchers("/finance/reports") .access(new WebExpressionAuthorizationManager( "hasAnyRole('MANAGER','ADMIN')" ));

الآثار الأمنية والمزالق الشائعة

  • غياب قاعدة الاصطياد الشامل: بدون .anyRequest() في النهاية، أي URL غير متطابق يتجاوز تكوين الأمان كليًّا. أنهِ دائمًا بـ .anyRequest().denyAll() أو .anyRequest().authenticated().
  • أحرف بديلة شاملة للغاية: يطابق /api/** كلًّا من /api/admin/users و /api/products. تأكد من أن قواعدك الأشمل تأتي بعد قواعدك الأضيق نطاقًا.
  • الاعتماد على أمان URL وحده: إذا استدعى مستخدم يمتلك ROLE_CUSTOMER فقط طريقةً في خدمة إدارية مباشرةً، فقواعد URL لن تحميك. ادمجها مع أمان مستوى الأساليب (الدرس التالي) لتحقيق دفاع متعدد الطبقات.
  • كشف نقاط نهاية Actuator: من السهل نسيان نقاط نهاية /actuator/** في Spring Boot. قيّدها دائمًا صراحةً لـ ROLE_OPS أو قائمة بيضاء من عناوين IP.

الخلاصة

تُعلَن قواعد التفويض في Spring Security 6 باستخدام authorizeHttpRequests داخل bean من نوع SecurityFilterChain. تُطابَق القواعد من الأعلى إلى الأسفل — يجب أن تسبق المسارات المحددة الأحرف البديلة الشاملة. استخدم hasRole() للتحقق القائم على الأدوار (يُضاف البادئة ROLE_ تلقائيًا) و hasAuthority() للنصوص الحرفية الدقيقة كنطاقات OAuth 2. أنهِ كل مجموعة قواعد بـ .anyRequest().denyAll() أو .anyRequest().authenticated() لمنع الكشف العرضي عن نقاط النهاية غير المتطابقة. في الدرس التالي ستنقل التفويض إلى مستوى الأساليب باستخدام @PreAuthorize و @PostAuthorize.