أساسيات Spring Security

إعداد الأمان باستخدام SecurityFilterChain

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

إعداد الأمان باستخدام SecurityFilterChain

أزال Spring Security 6 (المرفق مع Spring Boot 3) الفئة WebSecurityConfigurerAdapter كليًا. كان الأسلوب القديم يستلزم توسيع فئة مجردة وتجاوز عدد من التوابع المحمية — وهو تصميم صارم يجبرك على حشر كل إعداد أمني في هرمية وراثة واحدة. أما الأسلوب الحديث فهو قائم على المكونات بالكامل: تُعلن عن @Bean من نوع SecurityFilterChain داخل أي فئة @Configuration وتُهيّئه بواسطة واجهة برمجية طليقة تعتمد على الدوال اللامدائية (Lambda DSL). يستعرض هذا الدرس هذا النمط، ويشرح كل خيار رئيسي، ويوضح سبب كون التصميم الجديد أكثر قابلية للقراءة والاختبار.

سبب إزالة WebSecurityConfigurerAdapter

قبل Spring Security 5.7، كانت الطريقة المعتمدة لإعداد الأمان هي:

// قديم — لا تستخدم هذا النمط في Spring Boot 3 @Configuration public class OldSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin(); } }

كانت المشكلات حقيقية: جعل التوسع بالوراثة من المستحيل أن تُسهم أكثر من فئة إعداد بقواعد جزئية، وكان تسلسل and() عُرضة للأخطاء، كما صعّبت تبعية الوراثة اختبار الفئة في وحدات منفصلة. يزيل التصميم الجديد كل ذلك.

الإعداد الحديث الأدنى

كل ما تحتاجه هو فئة @Configuration عادية بتابع @Bean واحد:

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 .requestMatchers("/public/**", "/actuator/health").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard", true) .permitAll() ) .logout(logout -> logout .logoutSuccessUrl("/login?logout") .permitAll() ); return http.build(); } }

كل استدعاء من http.authorizeHttpRequests() وformLogin() وlogout() وغيرها يقبل الآن مُهيِّئًا لامدائيًا (lambda configurator) — وهو Consumer<T> يستقبل كائن DSL ذا نوع محدد. هذا يلغي استدعاءات and() ويجعل كل قسم مكتفيًا بذاته وسهل القراءة.

التعليم @EnableWebSecurity اختياري تقنيًا عند وجود الإعداد التلقائي لـ Spring Boot في مسار الفئات، لأن Boot يطبقه نيابةً عنك. إعلانه صراحةً ممارسة جيدة: يُوضح النية، ويُفعّل تسجيل الأمان على مستوى التتبع، ويضمن التقاط الإعداد حتى لو أعدت هيكلة السياق بعيدًا عن إدارة Boot.

قواعد التفويض — الترتيب مهم

تُقيَّم القواعد داخل authorizeHttpRequests من الأعلى إلى الأسفل، والقاعدة الأولى المطابقة هي الفائزة. ضع دائمًا الأنماط الأكثر تحديدًا أولًا:

.authorizeHttpRequests(auth -> auth // 1. الأصول العامة ونقطة نهاية تسجيل الدخول — لا يلزم مصادقة .requestMatchers("/css/**", "/js/**", "/images/**").permitAll() .requestMatchers("/login", "/register", "/forgot-password").permitAll() // 2. مناطق خاصة بأدوار معينة .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/api/internal/**").hasAnyRole("ADMIN", "SYSTEM") // 3. أي مستخدم مصادق يمكنه الوصول إلى البقية .anyRequest().authenticated() )
وضع .anyRequest().authenticated() قبل القواعد الأكثر تحديدًا سيُظلّلها ويجعلها غير فعّالة. أي قاعدة تُوضع بعد anyRequest() تُتجاهل بصمت. اجعل anyRequest() دائمًا آخر مدخل في الكتلة.

تعطيل CSRF لواجهات REST

حماية CSRF مُفعَّلة افتراضيًا لأن الموقف الافتراضي لـ Spring Security يفترض تطبيقًا يعتمد على الجلسة عبر المتصفح. لواجهة REST عديمة الحالة تستخدم مصادقة قائمة على الرمز المميز (مثل JWT)، فإن CSRF غير ضرورية ويجب تعطيلها:

import org.springframework.security.config.http.SessionCreationPolicy; @Bean public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ); return http.build(); }

تُخبر SessionCreationPolicy.STATELESS إطار Spring Security بعدم إنشاء HttpSession أو الرجوع إليها قط. مقرونًا بتعطيل CSRF، هذا هو الأساس الصحيح لأي خدمة مصغّرة مؤمَّنة بـ JWT.

لا تُعطّل CSRF أبدًا لتطبيق ويب تقليدي يعرض صفحات من الخادم. رموز CSRF تحمي التدفقات القائمة على ملفات تعريف الارتباط للجلسة. عطّل CSRF فقط عندما تتحقق من أن آلية المصادقة لديك غير قابلة للاستغلال (مثل رموز Bearer التي لا يمكن إرسالها من مواقع خارجية دون تعاون JavaScript).

تعدد حبوب SecurityFilterChain

من أكبر مكاسب النموذج القائم على المكونات أنه يمكنك امتلاك عدة حبوب SecurityFilterChain في نفس التطبيق، تحرس كل منها نطاق URL مختلفًا بقواعد مختلفة. استخدم @Order للتحكم في أي سلسلة تُقيَّم أولًا:

import org.springframework.core.annotation.Order; @Configuration @EnableWebSecurity public class MultiChainSecurityConfig { // السلسلة 1 — واجهة API عديمة الحالة تحت /api/** @Bean @Order(1) public SecurityFilterChain apiChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") // تحديد نطاق هذه السلسلة بـ /api/** .csrf(csrf -> csrf.disable()) .sessionManagement(s -> s .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ); return http.build(); } // السلسلة 2 — واجهة ويب تقليدية بنموذج تسجيل دخول @Bean @Order(2) public SecurityFilterChain webChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/about", "/login").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form.loginPage("/login").permitAll()) .logout(logout -> logout.permitAll()); return http.build(); } }

المفتاح هو securityMatcher() — فهو يحدد الطلبات التي تتعامل معها السلسلة. السلسلة التي لا تحمل securityMatcher تعمل كمصيدة شاملة ويجب أن تحمل أعلى رقم في @Order (أدنى أولوية).

ربط UserDetailsService مخصص وPasswordEncoder

لا تربط حبة SecurityFilterChain المصادقة مباشرةً — ذلك مهمة AuthenticationManager المدعوم بـ UserDetailsService وPasswordEncoder. أعلن عنهما كحبوب منفصلة في نفس الفئة @Configuration (أو في أي فئة أخرى):

import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService userDetailsService(UserRepository userRepo) { return username -> userRepo.findByUsername(username) .map(AppUserDetails::new) // تكييف الكيان الخاص بك مع UserDetails .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); }

يكتشف Spring Security هذه الحبوب تلقائيًا ويربطها بـ AuthenticationManager الافتراضي. لا حاجة لتجاوز أي شيء أو تسجيلها يدويًا.

الخلاصة

نمط حبة SecurityFilterChain الحديث أكثر وضوحًا وقابلية للتركيب والاختبار. استبدل أي فئة فرعية قديمة من WebSecurityConfigurerAdapter بفئة @Configuration تحتوي على تابع أو أكثر من نوع @Bean SecurityFilterChain. استخدم Lambda DSL لإعداد قواعد التفويض وسياسة CSRF وإدارة الجلسات وتدفقات تسجيل الدخول/الخروج في أقسام مستقلة. عندما تحتاج إلى قواعد أمان مختلفة لأجزاء مختلفة من تطبيقك — مثلًا واجهة API عديمة الحالة إلى جانب واجهة ويب تقليدية — أعلن عن سلاسل متعددة مع @Order صريح وتحديد نطاق بـ securityMatcher(). في الدرس القادم ستُنفّذ UserDetailsService وتدمجه مع مخزن مستخدمين حقيقي يعتمد على قاعدة بيانات.