أساسيات Spring Security

الأمان على مستوى الدوال

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

الأمان على مستوى الدوال

إنّ SecurityFilterChain الذي هيّأته في الدروس السابقة يحرس مسارات URL — وهو بمثابة السياج المحيطي. لكن ماذا يحدث حين يستدعي طرفا HTTP مختلفان نفس دالة الخدمة، أو حين تستدعي مهمة مجدولة داخلية منطق عمل يجب أن يُطبّق قواعد الوصول رغم ذلك؟ لا يستطيع تفويض الوصول على مستوى URL الإجابة على هذا السؤال. الأمان على مستوى الدوال ينقل حدود التطبيق إلى طبقة الخدمة، وهو المكان الذي ينتمي إليه لأي تطبيق يتجاوز البساطة.

يوفّر Spring Security تعليقَين توضيحيَّين رئيسيَّين لهذا الغرض: @PreAuthorize و@Secured. يغطي هذا الدرس كليهما بعمق — ما يفعله كل منهما، وكيف يختلفان، ومتى تُفضّل أحدهما على الآخر، والتداعيات الأمنية لكل خيار.

تفعيل الأمان على مستوى الدوال

أمان الدوال خاصية اختيارية. أضف @EnableMethodSecurity إلى أي فئة إعداد (فئة التطبيق الرئيسية أو فئة إعداد أمان مخصصة). في Spring Security 6 يحلّ هذا التعليق محل @EnableGlobalMethodSecurity القديم.

import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @Configuration @EnableMethodSecurity // يُفعّل @PreAuthorize و@PostAuthorize و@PreFilter و@PostFilter public class MethodSecurityConfig { // لا توجد حبوب إضافية مطلوبة للاستخدام الأساسي }
لماذا هي اختيارية؟ تفعيل أمان الدوال يضيف وكيل AOP من Spring حول كل حبّة (bean) تحمل دوالها تعليقات أمان. إن وضعت تعليقًا على حبّة لا تمر عبر Spring (مثل كائن أنشأته مباشرةً بـ new)، يُتجاهَل التعليق بصمت. الطبيعة الاختيارية تُبقي السلوك واضحًا وقابلًا للاختبار.

@PreAuthorize — تحكم دقيق قائم على التعابير

يُقيّم @PreAuthorize تعبير Spring Expression Language (SpEL) قبل تشغيل جسم الدالة. إذا أعاد التعبير false، يرمي Spring استثناء AccessDeniedException ولا تُدخل الدالة أبدًا. هذا هو التعليق الذي ينبغي اللجوء إليه في كل مشروع جديد تقريبًا.

import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; @Service public class ArticleService { // فقط المستخدمون بدور ADMIN يمكنهم حذف أي مقالة @PreAuthorize("hasRole('ADMIN')") public void deleteArticle(Long id) { articleRepository.deleteById(id); } // المستخدمون بدور EDITOR أو ADMIN يمكنهم النشر @PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')") public Article publish(Long id) { Article a = articleRepository.findById(id).orElseThrow(); a.setPublished(true); return articleRepository.save(a); } // يمكن للمستخدم تعديل مقالته الخاصة، أو يمكن للـ ADMIN تعديل مقالة أي شخص @PreAuthorize("hasRole('ADMIN') or #article.authorId == authentication.principal.id") public Article update(Article article) { return articleRepository.save(article); } }

بنية #article في المثال الثالث هي طريقة SpEL للإشارة إلى معامل دالة بالاسم. تمنحك authentication.principal كائن المستخدم المُصادَق عليه حاليًا (تنفيذك المخصص لـ UserDetails). هذا يعني أن منطق التفويض يمكنه فحص بيانات نطاق حقيقية — لا مجرد سلاسل أدوار — وهذا ما يجعل @PreAuthorize بالغ القوة.

تعابير SpEL الشائعة

  • hasRole('ADMIN') — يتحقق من وجود ROLE_ADMIN في صلاحيات المستخدم الممنوحة (يُضيف Spring البادئة ROLE_ تلقائيًا).
  • hasAnyRole('EDITOR','VIEWER') — صحيح إذا كان المستخدم يملك أيًا من الأدوار المدرجة.
  • hasAuthority('ARTICLE_DELETE') — يتحقق من سلسلة الصلاحية بالضبط؛ مفيد لأنظمة الأذونات الدقيقة التي تتجاوز أسماء الأدوار.
  • isAuthenticated() — صحيح لأي مستخدم مسجّل دخوله (غير مجهول).
  • isAnonymous() — صحيح فقط للطلبات غير المُصادَق عليها.
  • authentication.name == 'system' — مقارنة اسم المستخدم مباشرةً.
  • #param — الإشارة إلى معامل دالة باسمه param.
  • @myBean.check(#param) — التفويض إلى دالة حبّة Spring لمنطق معقد.
فوّض القواعد المعقدة إلى حبّة Spring. حين يتضمّن منطق التفويض استعلام قاعدة بيانات (مثل "هل هذا المستخدم عضو في المشروع؟")، ضع ذلك المنطق في حبّة Spring — مثلًا @Component SecurityService — وأشر إليها في SpEL: @PreAuthorize("@securityService.isProjectMember(#projectId, authentication)"). هذا يُبقي تعليقاتك مقروءة ومنطقك قابلًا لاختبارات الوحدة.

@PostAuthorize — التفويض بناءً على القيمة المُعادة

أحيانًا لا تعرف ما إذا كان ينبغي منح الوصول حتى بعد تحميل الدالة للبيانات من قاعدة البيانات. يُشغّل @PostAuthorize تعبير SpEL الخاص به بعد إعادة الدالة للنتيجة، مع إتاحة القيمة المُعادة عبر returnObject.

// جلب الوثيقة أولًا، ثم التحقق من ملكية المُستدعي @PostAuthorize("returnObject.ownerId == authentication.principal.id or hasRole('ADMIN')") public Document getDocument(Long id) { return documentRepository.findById(id).orElseThrow(); }
@PostAuthorize لا تزال تُنفّذ الدالة. تحدث قراءة قاعدة البيانات قبل فحص التفويض. للعمليات للقراءة فقط هذا مقبول في الغالب، لكن لا تستخدم @PostAuthorize أبدًا على دالة لها آثار جانبية (كتابة، بريد إلكتروني، مدفوعات) — استخدم @PreAuthorize مع فحص أذونات مُحمَّل مسبقًا بدلًا من ذلك.

@Secured — فحص الأدوار البسيط

@Secured تعليق قديم يقبل قائمة من سلاسل الصلاحيات. تُنفَّذ الدالة فقط إذا كان المستخدم المُصادَق عليه يملك واحدة منها على الأقل. خلافًا لـ @PreAuthorize، لا يدعم SpEL — لا مراجع للمعاملات، ولا تفويض للحبوب، ولا تعابير مركّبة.

import org.springframework.security.access.annotation.Secured; @Service public class ReportService { @Secured("ROLE_ADMIN") public byte[] generateFullReport() { // ... } @Secured({"ROLE_ADMIN", "ROLE_AUDITOR"}) public List<AuditEntry> getAuditLog() { // ... } }

لاحظ أن البادئة الكاملة ROLE_ مطلوبة مع @Secured، في حين يُضيفها hasRole() في SpEL تلقائيًا.

لتفعيل @Secured يجب تمكينها صراحةً:

@EnableMethodSecurity(securedEnabled = true) // يُفعّل أيضًا @PreAuthorize بشكل افتراضي public class MethodSecurityConfig { }

@PreAuthorize مقابل @Secured — متى تستخدم أيًّا منهما

  • استخدم @PreAuthorize في كل الكود الجديد. فهو أكثر تعبيرًا، يدعم SpEL، يعمل مع الأدوار والصلاحيات الدقيقة، ويمكنه فحص معاملات الدوال.
  • استخدم @Secured فقط حين تحتاج إلى دعم قاعدة كود قديمة تستخدمه بالفعل، أو حين تريد إشارة بصرية بأن الدالة تُجري فحص دور بسيطًا لا أكثر.

تطبيق التعليقات على مستوى الفئة

يمكنك وضع @PreAuthorize على فئة لتعيين سياسة افتراضية لجميع دوالها، ثم تجاوزها لكل دالة حسب الحاجة:

@Service @PreAuthorize("isAuthenticated()") // كل دالة تتطلب تسجيل دخول public class InvoiceService { public List<Invoice> listMyInvoices() { /* ... */ } // يرث isAuthenticated() @PreAuthorize("hasRole('ADMIN')") // يتجاوز: ADMIN فقط هنا public void deleteInvoice(Long id) { /* ... */ } }

اختبار أمان الدوال

يوفّر Spring Security Test التعليقَين @WithMockUser و@WithUserDetails لمحاكاة سياقات مُصادَقة في اختبارات الوحدة. يتيح لك هذا التحقق من قواعد التفويض دون خادم قيد التشغيل.

import org.springframework.security.test.context.support.WithMockUser; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class ArticleServiceTest { @Autowired ArticleService articleService; @Test @WithMockUser(roles = "ADMIN") void adminCanDelete() { assertDoesNotThrow(() -> articleService.deleteArticle(1L)); } @Test @WithMockUser(roles = "EDITOR") void editorCannotDelete() { assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> articleService.deleteArticle(1L)); } }
اختبر الحالات السلبية. تعليق @PreAuthorize المفقود هو ثغرة أمنية صامتة. اكتب دائمًا اختبارات تُؤكد رفض المستخدمين غير المُخوَّلين، لا فقط السماح للمُخوَّلين.

مزالق عملية

  • الاستدعاء الذاتي يتجاوز الوكيل. إذا استدعت دالة في نفس الفئة دالةً أخرى مُعلَّقة مباشرةً (لا عبر وكيل Spring)، لا يُقيَّم التعليق. استخرج الدالة إلى حبّة منفصلة إذا احتجت تطبيقها على الاستدعاءات الداخلية.
  • الواجهة مقابل التنفيذ. ضع التعليقات على دوال فئة التنفيذ، لا على دوال الواجهة، إلا إذا كنت تستخدم وكلاء قائمة على الواجهة (نادر في Spring Boot). يدعم Spring Security 6 كليهما، لكن التعليق على التنفيذ أأمن وأوضح.
  • لا تستبدل أمان URL. أمان الدوال يكمّل تفويض URL — لا يحلّ محلّه. احتفظ بكلتا الطبقتين. قواعد URL ترفض الطلبات السيئة عند المحيط؛ وقواعد الدوال تُطبّق ثوابت العمل في أعماق المكدس.

الخلاصة

يتيح الأمان على مستوى الدوال إرفاق قواعد تفويض مباشرةً بدوال الخدمة باستخدام @PreAuthorize (SpEL، تعبيري، مُفضَّل) أو @Secured (سلاسل أدوار بسيطة، متوافق مع الكود القديم). فعّل الميزة بـ @EnableMethodSecurity على فئة إعداد. استخدم مراجع معاملات SpEL وتفويض الحبوب للحفاظ على قابلية اختبار القواعد المعقدة. اقرن دائمًا أمان الدوال بتفويض URL — الطبقتان معًا توفران دفاعًا متعمقًا. في الدرس القادم ستجمع كل شيء معًا بتأمين تطبيق ويب كامل من البداية إلى النهاية.