أساسيات Spring Security

سلسلة فلاتر الأمان

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

سلسلة فلاتر الأمان

كل طلب HTTP يصل إلى تطبيق Spring Boot يمرّ عبر خط أنابيب من فلاتر servlet قبل أن يلمس وحدات التحكم الخاصة بك. يندمج Spring Security في هذا الخط بسلسلته المرتّبة الخاصة من الفلاتر — سلسلة فلاتر الأمان (Security Filter Chain). فهم هذه السلسلة أمر جوهري: فهي من يحدّد ما يحدث للطلبات غير المصادَق عليها، وكيف تُستخرج بيانات الاعتماد، وكيف تُدار الجلسات، وما هي استجابات الخطأ المُرسَلة. إن بدا سلوك أمني ما غير متوقّع فسلسلة الفلاتر هي دائمًا أول مكان عليك النظر فيه.

كيف تعمل فلاتر Servlet

تُعرّف مواصفة Java Servlet واجهةً باسم Filter بتابع وحيد هو doFilter(request, response, chain). يستطيع الفلتر فحص الطلب وتعديله، ثم استدعاء chain.doFilter() لتمرير التحكم إلى الفلتر التالي في السلسلة، ثم فحص الاستجابة وتعديلها في طريق العودة. الفلاتر مرتّبة وكل فلتر يلفّ الذي يليه — مكوّنةً سلسلةً حقيقية.

يسجّل Spring Security فلترًا واحدًا في سلسلة فلاتر حاوية servlet العادية يُسمّى DelegatingFilterProxy. يفوّض هذا الوكيل كل الأعمال إلى حبّة Spring مُدارة باسم springSecurityFilterChain. خلف تلك الحبّة يقبع FilterChainProxy الذي يحتوي على مثيل أو أكثر من SecurityFilterChain — كل منها قائمة من فلاتر Spring Security المحدّدة.

الفكرة الجوهرية: Spring Security ليس سحرًا — بل هو قائمة مرتّبة بعناية من فلاتر servlet عادية. كل قرار مصادقة وكل إعادة توجيه إلى صفحة تسجيل الدخول وكل استجابة 403 تنشأ في أحد هذه الفلاتر.

الترتيب الافتراضي للفلاتر

عند إضافة spring-boot-starter-security إلى المشروع يُهيّئ Spring Boot تلقائيًا SecurityFilterChain بالفلاتر التالية (مختصرة، بالترتيب):

  1. DisableEncodeUrlFilter — يمنع تسرّب معرّفات الجلسات في عناوين URL.
  2. WebAsyncManagerIntegrationFilter — يُوزّع SecurityContext على خيوط async.
  3. SecurityContextHolderFilter — يُحمّل SecurityContext من المستودع (عادةً جلسة HTTP) في بداية الطلب ويُنظّفه بعدها.
  4. HeaderWriterFilter — يُضيف ترويسات استجابة HTTP المتعلقة بالأمان (X-Frame-Options وX-Content-Type-Options وغيرها).
  5. CsrfFilter — يتحقق من رموز CSRF في الطلبات التي تغيّر الحالة.
  6. LogoutFilter — يعترض عنوان URL لتسجيل الخروج ويُنظّف سياق الأمان والجلسة.
  7. UsernamePasswordAuthenticationFilter — يعالج طلبات POST لتسجيل الدخول عبر النموذج.
  8. DefaultLoginPageGeneratingFilter — يخدم نموذج تسجيل الدخول المدمج (يُزال عند تقديم نموذجك الخاص).
  9. BearerTokenAuthenticationFilter — يستخرج رموز JWT أو الرموز المعتمة من ترويسة Authorization: Bearer (موجود فقط عند تهيئة OAuth2 Resource Server).
  10. RequestCacheAwareFilter — يُعيد تشغيل الطلب الأصلي بعد نجاح إعادة توجيه تسجيل الدخول.
  11. SecurityContextHolderAwareRequestWrapper — يلفّ الطلب لكشف Servlet security API.
  12. AnonymousAuthenticationFilter — إذا لم يُعيَّن أي مصادقة حتى الآن يُدخل مدير مجهول حتى لا يرى الكود اللاحق null أبدًا.
  13. ExceptionTranslationFilter — يلتقط AccessDeniedException وAuthenticationException ويحوّلهما إلى استجابات HTTP (401/302 أو 403).
  14. AuthorizationFilter — يُطبّق قواعد الوصول التي أعلنتها في SecurityFilterChain.
لست بحاجة لحفظ هذه القائمة. ما يهمّ هو النمط: تحميل سياق الأمان → المصادقة → الاحتياط بالمجهول → ترجمة الاستثناءات → التفويض. كل مرحلة تعتمد على ما قبلها.

كيف يتدفق الطلب عبر السلسلة

لنتأمّل طلب GET غير مصادَق عليه يستهدف موردًا محميًا:

  1. يبحث SecurityContextHolderFilter عن SecurityContext موجود في الجلسة — لا يجد شيئًا فيضع سياقًا فارغًا.
  2. تبحث فلاتر المصادقة (UsernamePassword وBearer وغيرها) عن بيانات اعتماد في الطلب — لا تجد شيئًا فتمرّ دون تعيين مدير.
  3. يرى AnonymousAuthenticationFilter أنه لا مصادقة مضبوطة فيُدخل AnonymousAuthenticationToken.
  4. يُقيّم AuthorizationFilter القاعدة المرتبطة بعنوان URL المطلوب (مثل authenticated()) في مواجهة الرمز المجهول — فيرفض الوصول.
  5. يلتقط ExceptionTranslationFilter الـ AccessDeniedException. بما أن المدير الحالي مجهول يتعامل معه باعتباره مصادقة مفقودة ويُعيد توجيه المتصفح إلى صفحة تسجيل الدخول (أو يُعيد 401 للواجهات البرمجية عديمة الحالة).

الآن لنتأمّل طلب POST إلى /login ببيانات اعتماد صحيحة:

  1. يُطابق UsernamePasswordAuthenticationFilter عنوان URL ويستخرج اسم المستخدم وكلمة المرور ثم يفوّض إلى AuthenticationManager.
  2. يُحمّل AuthenticationManager المستخدم عبر UserDetailsService ويتحقق من كلمة المرور ويُعيد كائن Authentication ممتلئًا.
  3. يخزّن الفلتر هذا في SecurityContext ويحفظ السياق في الجلسة.
  4. يُعيد AuthenticationSuccessHandler المُهيَّأ توجيه المستخدم إلى وجهته الأصلية.

سلاسل SecurityFilterChain متعددة

يسمح Spring Security 6 بتعريف حبّات SecurityFilterChain متعددة، كل منها مُطابَقة لنمط عنوان URL مختلف. هذا هو الأسلوب الاصطلاحي لتطبيق قواعد أمان مختلفة على — مثلًا — واجهة REST البرمجية (JWT عديمة الحالة) وواجهة المستخدم الإدارية (تسجيل دخول بالنموذج مع حالة).

@Configuration @EnableWebSecurity public class SecurityConfig { // السلسلة 1: تطبّق فقط على /api/** — عديمة الحالة، JWT @Bean @Order(1) public SecurityFilterChain apiChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .csrf(AbstractHttpConfigurer::disable) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } // السلسلة 2: تطبّق على كل شيء آخر — تسجيل دخول بالنموذج مع حالة @Bean @Order(2) public SecurityFilterChain webChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**", "/login", "/error").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()); return http.build(); } }

يُقيّم FilterChainProxy مُطابِق securityMatcher لكل سلسلة بترتيب @Order ويستخدم أول سلسلة مُطابِقة. لا يُستشار ما بعدها. نسيان تعيين مُطابِق على سلسلة ذات أولوية أدنى يجعلها فعليًا تلتقط كل شيء وقد تبتلع طلبات مخصّصة لسلاسلك الأخرى.

خطأ شائع — ترتيب السلاسل: إذا اشتركت حبّتان في نفس قيمة @Order سيرمي Spring استثناءً عند بدء التشغيل. اعطِ دائمًا ترتيبًا صريحًا وفريدًا لكل حبّة SecurityFilterChain في إعداد متعدد السلاسل.

إضافة فلتر مخصّص

يمكنك إدراج فلترك الخاص في موضع محدد داخل السلسلة باستخدام addFilterBefore أو addFilterAfter أو addFilterAt. حالة استخدام شائعة هي التحقق من ترويسة طلب مخصّصة قبل أن تعمل فلاتر المصادقة القياسية:

@Component public class ApiKeyFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String apiKey = request.getHeader("X-Api-Key"); if ("expected-secret".equals(apiKey)) { // بناء مصادقة موثوقة وتخزينها UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( "api-client", null, List.of(new SimpleGrantedAuthority("ROLE_API"))); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } } // داخل SecurityFilterChain الخاص بك: http.addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class);

يضمن التوسّع من OncePerRequestFilter تشغيل فلترك مرةً واحدةً بالضبط لكل طلب — وهذا مهمّ في حاويات servlet التي قد تُرسل داخليًا (مثل forward وerror dispatches).

فحص السلسلة أثناء التشغيل

أثناء التطوير يمكنك تسجيل كل فلتر يعالج طلبًا بضبط علامة debug في Spring Security:

# application.properties logging.level.org.springframework.security=TRACE

أو على مستوى التعليق التوضيحي:

@EnableWebSecurity(debug = true)
لا تُفعّل debug = true في الإنتاج أبدًا. فهو يُسجّل تفاصيل الطلبات الكاملة — الترويسات والمعاملات وقرارات الأمان — مما قد يكشف معلومات حساسة في مجمّعات السجلات.

سياق الأمان ونشره عبر الخيوط

يُخزَّن SecurityContext في ThreadLocal افتراضيًا. هذا يعني أنه متاح في أي مكان في مكدس استدعاءات نفس الخيط — وحدات تحكمك وخدماتك ومستودعاتك — دون تمريره صراحةً. لكن إن أنتجت خيطًا جديدًا (مثل CompletableFuture أو @Async أو خيوط افتراضية) فلن يُورَث السياق تلقائيًا. يوفّر Spring Security كلًّا من DelegatingSecurityContextExecutor واستراتيجية MODE_INHERITABLETHREADLOCAL للتعامل مع ذلك، وهو ما يُغطّى في درس أمان مستوى التابع.

الخلاصة

سلسلة فلاتر الأمان هي العمود الفقري لـ Spring Security. كل طلب يُعترَض بواسطة DelegatingFilterProxy ويُوجَّه عبر FilterChainProxy ويُعالَج بقائمة مرتّبة من الفلاتر التي تُحمّل سياق الأمان وتُحاول المصادقة وتحتاط بالمجهول ثم تُطبّق التفويض. تتيح لك السلاسل المتعددة تطبيق استراتيجيات أمان مختلفة على نطاقات عناوين URL مختلفة. تندرج الفلاتر المخصّصة بسهولة في هذه السلسلة في أي موضع. في الدرس التالي ستُحدّد الفرق بين المصادقة (من أنت؟) والتفويض (ماذا يمكنك أن تفعل؟).