Spring Security مع JWT لـ REST API
نشرت: 2022-03-11يعتبر الربيع إطار عمل موثوق به في نظام Java البيئي ويستخدم على نطاق واسع. لم يعد صالحًا الإشارة إلى الربيع كإطار عمل ، لأنه مصطلح شامل يغطي أطر عمل مختلفة. أحد هذه الأطر هو Spring Security ، وهو إطار مصادقة وترخيص قوي وقابل للتخصيص. يعتبر المعيار الفعلي لتأمين التطبيقات القائمة على الربيع.
على الرغم من شعبيتها ، يجب أن أعترف أنه عندما يتعلق الأمر بالتطبيقات ذات الصفحة الواحدة ، فليس من السهل والمباشر تكوينها. أظن أن السبب هو أنه بدأ أكثر كإطار عمل موجه لتطبيق MVC ، حيث يحدث عرض صفحة الويب على جانب الخادم ويكون الاتصال قائمًا على الجلسة.
إذا كانت النهاية الخلفية تعتمد على Java و Spring ، فمن المنطقي استخدام Spring Security للمصادقة / التفويض وتهيئته للاتصال عديم الحالة. في حين أن هناك الكثير من المقالات التي تشرح كيفية القيام بذلك ، بالنسبة لي ، كان إعداده لأول مرة أمرًا محبطًا ، وكان عليّ قراءة المعلومات من مصادر متعددة وتلخيصها. لهذا السبب قررت كتابة هذا المقال ، حيث سأحاول تلخيص وتغطية جميع التفاصيل الدقيقة المطلوبة ونقاط الضعف التي قد تواجهها أثناء عملية التكوين.
تعريف المصطلحات
قبل الخوض في التفاصيل الفنية ، أريد تحديد المصطلحات المستخدمة في سياق Spring Security بشكل صريح للتأكد من أننا جميعًا نتحدث نفس اللغة.
هذه هي المصطلحات التي نحتاج إلى معالجتها:
- تشير المصادقة إلى عملية التحقق من هوية المستخدم ، بناءً على بيانات الاعتماد المقدمة. من الأمثلة الشائعة إدخال اسم مستخدم وكلمة مرور عند تسجيل الدخول إلى موقع ويب. يمكنك التفكير في الأمر على أنه إجابة على السؤال من أنت؟ .
- يشير التفويض إلى عملية تحديد ما إذا كان المستخدم لديه الإذن المناسب لتنفيذ إجراء معين أو قراءة بيانات معينة ، بافتراض أن المستخدم قد تمت المصادقة عليه بنجاح. يمكنك التفكير في الأمر على أنه إجابة على السؤال هل يمكن للمستخدم قراءة / قراءة هذا؟ .
- يشير المبدأ إلى المستخدم المصدق عليه حاليًا.
- تشير السلطة الممنوحة إلى إذن المستخدم المصادق عليه.
- يشير الدور إلى مجموعة أذونات المستخدم المصادق عليه.
إنشاء تطبيق الربيع الأساسي
قبل الانتقال إلى تكوين إطار عمل Spring Security ، فلنقم بإنشاء تطبيق ويب Spring أساسي. لهذا ، يمكننا استخدام Spring Initializr وإنشاء مشروع نموذجي. بالنسبة لتطبيق ويب بسيط ، فإن تبعية إطار عمل الويب Spring تكفي فقط:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
بمجرد إنشاء المشروع ، يمكننا إضافة وحدة تحكم REST بسيطة إليه على النحو التالي:
@RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }
بعد ذلك ، إذا أنشأنا المشروع وقمنا بتشغيله ، فيمكننا الوصول إلى عناوين URL التالية في متصفح الويب:
-
http://localhost:8080/hello/user
السلسلةHello User
. -
http://localhost:8080/hello/admin
سيعيد السلسلةHello Admin
.
الآن ، يمكننا إضافة إطار عمل Spring Security إلى مشروعنا ، ويمكننا القيام بذلك عن طريق إضافة التبعية التالية إلى ملف pom.xml
بنا:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
لا تؤدي إضافة تبعيات إطار عمل Spring الأخرى عادةً إلى تأثير فوري على أحد التطبيقات حتى نقدم التكوين المقابل ، ولكن Spring Security مختلف من حيث أنه له تأثير فوري ، وهذا عادةً ما يربك المستخدمين الجدد. بعد إضافته ، إذا أعدنا بناء المشروع وتشغيله ثم حاولنا الوصول إلى أحد عناوين URL المذكورة أعلاه بدلاً من عرض النتيجة ، فسيتم إعادة توجيهنا إلى http://localhost:8080/login
. هذا هو السلوك الافتراضي لأن إطار عمل Spring Security يتطلب مصادقة خارج الصندوق لجميع عناوين URL.
لتمرير المصادقة ، يمكننا استخدام اسم user
الافتراضي والعثور على كلمة مرور تم إنشاؤها تلقائيًا في وحدة التحكم الخاصة بنا:
Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce
يرجى تذكر أن كلمة المرور تتغير في كل مرة نقوم فيها بإعادة تشغيل التطبيق. إذا أردنا تغيير هذا السلوك وجعل كلمة المرور ثابتة ، فيمكننا إضافة التكوين التالي إلى ملف application.properties
الخاص بنا:
spring.security.user.password=Test12345_
الآن ، إذا أدخلنا بيانات الاعتماد في نموذج تسجيل الدخول ، فسيتم إعادة توجيهنا مرة أخرى إلى عنوان URL الخاص بنا وسنرى النتيجة الصحيحة. يرجى ملاحظة أن عملية المصادقة الجاهزة تعتمد على الجلسة ، وإذا أردنا تسجيل الخروج ، فيمكننا الوصول إلى عنوان URL التالي: http://localhost:8080/logout
قد يكون هذا السلوك غير المألوف مفيدًا لتطبيقات الويب MVC الكلاسيكية حيث لدينا مصادقة تستند إلى الجلسة ، ولكن في حالة التطبيقات أحادية الصفحة ، عادةً لا يكون ذلك مفيدًا لأنه في معظم حالات الاستخدام ، لدينا جانب العميل التقديم والمصادقة عديمة الحالة المستندة إلى JWT. في هذه الحالة ، سيتعين علينا تخصيص إطار عمل Spring Security بشكل كبير ، وهو ما سنفعله في بقية المقالة.
على سبيل المثال ، سنقوم بتنفيذ تطبيق ويب كلاسيكي لبيع الكتب وإنشاء نهاية خلفية توفر واجهات برمجة تطبيقات CRUD لإنشاء مؤلفين وكتب بالإضافة إلى واجهات برمجة تطبيقات لإدارة المستخدم والمصادقة.
نظرة عامة على هندسة أمان الربيع
قبل أن نبدأ في تخصيص التكوين ، دعنا أولاً نناقش كيفية عمل مصادقة Spring Security خلف الكواليس.
يعرض الرسم البياني التالي التدفق ويوضح كيفية معالجة طلبات المصادقة:
هندسة الربيع الأمنية

الآن ، دعنا نقسم هذا الشكل إلى مكونات ونناقش كل منها على حدة.
سلسلة فلاتر الأمان الربيعية
عندما تضيف إطار عمل Spring Security إلى تطبيقك ، فإنه يسجل تلقائيًا سلسلة فلاتر تعترض جميع الطلبات الواردة. تتكون هذه السلسلة من مرشحات مختلفة ، ويتعامل كل منها مع حالة استخدام معينة.
علي سبيل المثال:
- تحقق مما إذا كان عنوان URL المطلوب متاحًا للجمهور ، بناءً على التكوين.
- في حالة المصادقة المستندة إلى الجلسة ، تحقق مما إذا كان المستخدم قد تمت مصادقته بالفعل في الجلسة الحالية.
- تحقق مما إذا كان المستخدم مصرحًا له بتنفيذ الإجراء المطلوب ، وما إلى ذلك.
أحد التفاصيل المهمة التي أريد أن أذكرها هو أن مرشحات Spring Security مسجلة بأدنى ترتيب وهي أول عوامل التصفية التي تم استدعاؤها. في بعض حالات الاستخدام ، إذا كنت تريد وضع الفلتر المخصص أمامهم ، فستحتاج إلى إضافة حشوة إلى ترتيبهم. يمكن القيام بذلك من خلال التكوين التالي:
spring.security.filter.order=10
بمجرد إضافة هذا التكوين إلى ملف application.properties
الخاص بنا ، سيكون لدينا مساحة لـ 10 مرشحات مخصصة أمام مرشحات Spring Security.
مدير المصادقة
يمكنك التفكير في برنامج AuthenticationManager
كمنسق حيث يمكنك تسجيل عدة موفرين ، وبناءً على نوع الطلب ، سيقدم طلب المصادقة إلى المزود الصحيح.
مصادقة مزود
AuthenticationProvider
يعالج أنواعًا معينة من المصادقة. تعرض واجهته وظيفتين فقط:
-
authenticate
تؤدي المصادقة مع الطلب. -
supports
عمليات التحقق مما إذا كان هذا الموفر يدعم نوع المصادقة المشار إليه.
أحد التطبيقات المهمة للواجهة التي نستخدمها في مشروعنا النموذجي هو DaoAuthenticationProvider
، الذي يسترد تفاصيل المستخدم من UserDetailsService
.
UserDetailsService
يتم وصف UserDetailsService
أساسية تقوم بتحميل بيانات خاصة بالمستخدم في وثائق Spring.
في معظم حالات الاستخدام ، يقوم موفرو المصادقة باستخراج معلومات هوية المستخدم بناءً على بيانات الاعتماد من قاعدة بيانات ثم إجراء التحقق من الصحة. نظرًا لأن حالة الاستخدام هذه شائعة جدًا ، فقد قرر مطورو Spring استخراجها كواجهة منفصلة ، والتي تكشف عن الوظيفة الفردية:
- يقبل
loadUserByUsername
اسم المستخدم كمعامل ويعيد كائن هوية المستخدم.
المصادقة باستخدام JWT مع Spring Security
بعد مناقشة العناصر الداخلية لإطار Spring Security ، فلنقم بتكوينه للمصادقة عديمة الحالة باستخدام رمز JWT.
لتخصيص Spring Security ، نحتاج إلى فئة التكوين مع التعليق التوضيحي @EnableWebSecurity
في مسار الفصل الخاص بنا. أيضًا ، لتبسيط عملية التخصيص ، يعرض إطار العمل فئة WebSecurityConfigurerAdapter
. سنقوم بتمديد هذا المحول وإلغاء كلتا وظيفتيه من أجل:
- تكوين مدير المصادقة مع الموفر الصحيح
- تكوين أمان الويب (عناوين URL العامة ، عناوين URL الخاصة ، التفويض ، إلخ.)
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }
في نموذج التطبيق الخاص بنا ، نقوم بتخزين هويات المستخدمين في قاعدة بيانات MongoDB ، في مجموعة users
. يتم تعيين هذه الهويات بواسطة كيان User
، ويتم تحديد عمليات CRUD الخاصة بهم بواسطة مستودع بيانات UserRepo
Spring.
الآن ، عندما نقبل طلب المصادقة ، نحتاج إلى استرداد الهوية الصحيحة من قاعدة البيانات باستخدام بيانات الاعتماد المقدمة ثم التحقق منها. لهذا ، نحتاج إلى تنفيذ واجهة UserDetailsService
، والتي تم تعريفها على النحو التالي:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
هنا ، يمكننا أن نرى أنه من الضروري إرجاع الكائن الذي يقوم بتنفيذ واجهة UserDetails
، ويقوم كيان User
لدينا بتنفيذها (للحصول على تفاصيل التنفيذ ، يرجى الاطلاع على مستودع نموذج المشروع). بالنظر إلى حقيقة أنه لا يعرض سوى النموذج الأولي أحادي الوظيفة ، يمكننا التعامل معه كواجهة وظيفية وتوفير التنفيذ كتعبير لامدا.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } // Details omitted for brevity }
هنا ، سيؤدي استدعاء دالة auth.userDetailsService
إلى بدء مثيل DaoAuthenticationProvider
باستخدام تطبيقنا لواجهة UserDetailsService
وتسجيله في مدير المصادقة.
جنبًا إلى جنب مع موفر المصادقة ، نحتاج إلى تكوين مدير مصادقة باستخدام مخطط تشفير كلمة المرور الصحيح الذي سيتم استخدامه للتحقق من بيانات الاعتماد. لهذا ، نحتاج إلى الكشف عن التطبيق المفضل لواجهة PasswordEncoder
كوحدة واحدة.
في مشروعنا النموذجي ، سنستخدم خوارزمية bcrypt password-hashing.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Details omitted for brevity }
بعد تكوين مدير المصادقة ، نحتاج الآن إلى تكوين أمان الويب. نحن ننفذ واجهة برمجة تطبيقات REST ونحتاج إلى مصادقة عديمة الحالة باستخدام رمز JWT ؛ لذلك ، نحتاج إلى تعيين الخيارات التالية:
- تمكين CORS وتعطيل CSRF.
- تعيين إدارة الجلسة إلى عديم الحالة.
- تعيين معالج استثناء الطلبات غير المصرح به.
- تعيين الأذونات على نقاط النهاية.
- أضف مرشح رمز JWT.
يتم تنفيذ هذا التكوين على النحو التالي:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; private final JwtTokenFilter jwtTokenFilter; public SecurityConfig(UserRepo userRepo, JwtTokenFilter jwtTokenFilter) { this.userRepo = userRepo; this.jwtTokenFilter = jwtTokenFilter; } // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Used by spring security if CORS is enabled. @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }
يرجى ملاحظة أننا أضفنا JwtTokenFilter
قبل Spring Security الداخلي UsernamePasswordAuthenticationFilter
. نحن نقوم بذلك لأننا نحتاج إلى الوصول إلى هوية المستخدم في هذه المرحلة لأداء المصادقة / التخويل ، ويتم استخراجها داخل مرشح JWT المميز بناءً على رمز JWT المقدم. يتم تنفيذ ذلك على النحو التالي:
@Component public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserRepo userRepo; public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserRepo userRepo) { this.jwtTokenUtil = jwtTokenUtil; this.userRepo = userRepo; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Get authorization header and validate final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith("Bearer ")) { chain.doFilter(request, response); return; } // Get jwt token and validate final String token = header.split(" ")[1].trim(); if (!jwtTokenUtil.validate(token)) { chain.doFilter(request, response); return; } // Get user identity and set it on the spring security context UserDetails userDetails = userRepo .findByUsername(jwtTokenUtil.getUsername(token)) .orElse(null); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails == null ? List.of() : userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }
قبل تنفيذ وظيفة واجهة برمجة تطبيقات تسجيل الدخول الخاصة بنا ، نحتاج إلى اتخاذ خطوة أخرى - نحتاج إلى الوصول إلى مدير المصادقة. بشكل افتراضي ، لا يمكن الوصول إليه بشكل عام ، ونحن بحاجة إلى كشفه بشكل صريح باعتباره أحد البرامج في فئة التكوين الخاصة بنا.

ويمكن القيام بذلك على النحو التالي:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
والآن ، نحن جاهزون لتنفيذ وظيفة واجهة برمجة تطبيقات تسجيل الدخول الخاصة بنا:
@Api(tags = "Authentication") @RestController @RequestMapping(path = "api/public") public class AuthApi { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; private final UserViewMapper userViewMapper; public AuthApi(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserViewMapper userViewMapper) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; this.userViewMapper = userViewMapper; } @PostMapping("login") public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) { try { Authentication authenticate = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); User user = (User) authenticate.getPrincipal(); return ResponseEntity.ok() .header( HttpHeaders.AUTHORIZATION, jwtTokenUtil.generateAccessToken(user) ) .body(userViewMapper.toUserView(user)); } catch (BadCredentialsException ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } }
هنا ، نتحقق من بيانات الاعتماد المقدمة باستخدام مدير المصادقة ، وفي حالة النجاح ، نقوم بإنشاء رمز JWT وإعادته كرأس استجابة جنبًا إلى جنب مع معلومات هوية المستخدم في نص الاستجابة.
التفويض مع Spring Security
في القسم السابق ، قمنا بإعداد عملية مصادقة وتكوين عناوين URL عامة / خاصة. قد يكون هذا كافيًا للتطبيقات البسيطة ، ولكن بالنسبة لمعظم حالات الاستخدام في العالم الحقيقي ، نحتاج دائمًا إلى سياسات وصول تستند إلى الأدوار لمستخدمينا. في هذا الفصل ، سنتناول هذه المشكلة ونقوم بإعداد مخطط تفويض مستند إلى الدور باستخدام إطار عمل Spring Security.
في نموذج التطبيق الخاص بنا ، حددنا الأدوار الثلاثة التالية:
- يسمح لنا
USER_ADMIN
بإدارة مستخدمي التطبيق. - يسمح لنا
AUTHOR_ADMIN
بإدارة المؤلفين. - يتيح لنا
BOOK_ADMIN
إدارة الكتب.
الآن ، نحتاج إلى تطبيقها على عناوين URL المقابلة:
-
api/public
متاحة للعامة. - يمكن لواجهة برمجة
api/admin/user
الوصول إلى المستخدمين الذين لهم دورUSER_ADMIN
. - يمكن لواجهة برمجة
api/author
الوصول إلى المستخدمين بدورAUTHOR_ADMIN
. - يمكن لواجهة برمجة
api/book
الوصول إلى المستخدمين من خلال دورBOOK_ADMIN
.
يوفر لنا إطار عمل Spring Security خيارين لإعداد مخطط التفويض:
- التكوين القائم على URL
- التكوين القائم على التعليقات التوضيحية
أولاً ، دعنا نرى كيف يعمل التكوين المستند إلى URL. يمكن تطبيقه على تكوين أمان الويب على النحو التالي:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN) .antMatchers("/api/author/**").hasRole(Role.AUTHOR_ADMIN) .antMatchers("/api/book/**").hasRole(Role.BOOK_ADMIN) .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Details omitted for brevity }
كما ترى ، هذا الأسلوب بسيط ومباشر ، لكن له جانب سلبي. يمكن أن يكون مخطط التفويض في تطبيقنا معقدًا ، وإذا حددنا جميع القواعد في مكان واحد ، فسيصبح كبيرًا جدًا ومعقدًا ويصعب قراءته. لهذا السبب ، أفضّل عادةً استخدام التكوين المستند إلى التعليقات التوضيحية.
يحدد إطار عمل Spring Security التعليقات التوضيحية التالية لأمان الويب:
- تدعم
@PreAuthorize
لغة Spring Expression وتستخدم لتوفير التحكم في الوصول المستند إلى التعبير قبل تنفيذ الطريقة. - تدعم
@PostAuthorize
لغة Spring Expression وتستخدم لتوفير التحكم في الوصول المستند إلى التعبير بعد تنفيذ الأسلوب (يوفر القدرة على الوصول إلى نتيجة الطريقة). - يدعم
@PreFilter
لغة Spring Expression ويستخدم لتصفية المجموعة أو المصفوفات قبل تنفيذ الطريقة بناءً على قواعد الأمان المخصصة التي نحددها. - يدعم
@PostFilter
لغة Spring Expression ويستخدم لتصفية المجموعة أو المصفوفات التي تم إرجاعها بعد تنفيذ الطريقة بناءً على قواعد الأمان المخصصة التي نحددها (توفر القدرة على الوصول إلى نتيجة الطريقة). -
@Secured
لا يدعم Spring Expression Language ويستخدم لتحديد قائمة الأدوار على طريقة. -
@RolesAllowed
لا يدعم Spring Expression Language وهو التعليق التوضيحي المكافئ لـ JSR 250 للتعليق التوضيحي@Secured
.
يتم تعطيل هذه التعليقات التوضيحية افتراضيًا ويمكن تمكينها في تطبيقنا على النحو التالي:
@EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }
securedEnabled = true
لتمكين التعليق التوضيحي @Secured
.
jsr250Enabled = true
لتمكين التعليق التوضيحي @RolesAllowed
.
prePostEnabled = true
يمكّن @PreAuthorize
، @ PostAuthorize ، @PostAuthorize
، @PreFilter
@PostFilter
التعليقات التوضيحية.
بعد تمكينهم ، يمكننا فرض سياسات الوصول القائمة على الأدوار على نقاط نهاية واجهة برمجة التطبيقات لدينا مثل هذا:
@Api(tags = "UserAdmin") @RestController @RequestMapping(path = "api/admin/user") @RolesAllowed(Role.USER_ADMIN) public class UserAdminApi { // Details omitted for brevity } @Api(tags = "Author") @RestController @RequestMapping(path = "api/author") public class AuthorApi { // Details omitted for brevity @RolesAllowed(Role.AUTHOR_ADMIN) @PostMapping public void create() { } @RolesAllowed(Role.AUTHOR_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.AUTHOR_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/book") public void getBooks() { } @PostMapping("search") public void search() { } } @Api(tags = "Book") @RestController @RequestMapping(path = "api/book") public class BookApi { // Details omitted for brevity @RolesAllowed(Role.BOOK_ADMIN) @PostMapping public BookView create() { } @RolesAllowed(Role.BOOK_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.BOOK_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/author") public void getAuthors() { } @PostMapping("search") public void search() { } }
يرجى ملاحظة أنه يمكن توفير التعليقات التوضيحية المتعلقة بالأمان على مستوى الفصل ومستوى الطريقة.
الأمثلة الموضحة بسيطة ولا تمثل سيناريوهات العالم الحقيقي ، ولكن Spring Security يوفر مجموعة غنية من التعليقات التوضيحية ، ويمكنك التعامل مع مخطط تفويض معقد إذا اخترت استخدامها.
البادئة الافتراضية اسم الدور
في هذا القسم الفرعي المنفصل ، أود التأكيد على أحد التفاصيل الدقيقة التي تربك الكثير من المستخدمين الجدد.
يفرق إطار Spring Security بين مصطلحين:
- تمثل
Authority
إذنًا فرديًا. - يمثل
Role
مجموعة من الأذونات.
يمكن تمثيل كلاهما بواجهة واحدة تسمى GrantedAuthority
وتم التحقق منها لاحقًا باستخدام Spring Expression Language داخل تعليقات Spring Security على النحو التالي:
-
Authority
:PreAuthorize (“hasAuthority ('EDIT_BOOK')”) -
Role
:PreAuthorize (“hasRole ('BOOK_ADMIN')”)
لجعل الفرق بين هذين المصطلحين أكثر وضوحًا ، يضيف إطار عمل Spring Security بادئة ROLE_
إلى اسم الدور افتراضيًا. لذلك ، بدلاً من البحث عن دور باسم BOOK_ADMIN
، سوف يتحقق من ROLE_BOOK_ADMIN
.
أنا شخصياً أجد هذا السلوك محيرًا وأفضل تعطيله في تطبيقاتي. يمكن تعطيله داخل تكوين Spring Security على النحو التالي:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }
الاختبار مع Spring Security
لاختبار نقاط النهاية الخاصة بنا من خلال اختبارات الوحدة أو التكامل عند استخدام إطار عمل Spring Security ، نحتاج إلى إضافة تبعية spring-security-test
جنبًا إلى جنب مع اختبار بدء spring-boot-starter-test
. سيبدو ملف بناء pom.xml
كما يلي:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
تتيح لنا هذه التبعية الوصول إلى بعض التعليقات التوضيحية التي يمكن استخدامها لإضافة سياق أمان إلى وظائف الاختبار الخاصة بنا.
هذه التعليقات التوضيحية هي:
- يمكن إضافة
@WithMockUser
إلى طريقة اختبار لمحاكاة التشغيل مع مستخدم تعرض للسخرية. - يمكن إضافة
@WithUserDetails
إلى طريقة اختبار لمحاكاة التشغيل باستخدامUserDetails
التي يتم إرجاعها منUserDetailsService
. - يمكن إضافة
@WithAnonymousUser
إلى طريقة اختبار لمحاكاة التشغيل مع مستخدم مجهول. يكون هذا مفيدًا عندما يريد المستخدم إجراء غالبية الاختبارات كمستخدم معين وتجاوز بعض الطرق ليكون مجهول الهوية. - يحدد
@WithSecurityContext
SecurityContext
الذي يجب استخدامه ، وتعتمد عليه جميع التعليقات التوضيحية الثلاثة الموضحة أعلاه. إذا كانت لدينا حالة استخدام محددة ، فيمكننا إنشاء تعليق توضيحي خاص بنا يستخدم@WithSecurityContext
لإنشاء أيSecurityContext
نريده. مناقشته خارج نطاق مقالتنا ، ويرجى الرجوع إلى وثائق Spring Security للحصول على مزيد من التفاصيل.
أسهل طريقة لإجراء الاختبارات مع مستخدم معين هي استخدام التعليق التوضيحي @WithMockUser
. يمكننا إنشاء مستخدم وهمي معها وإجراء الاختبار على النحو التالي:
@Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }
هذا النهج له بعض العيوب. أولاً ، المستخدم الوهمي غير موجود ، وإذا قمت بتشغيل اختبار التكامل ، والذي يستعلم لاحقًا عن معلومات المستخدم من قاعدة البيانات ، فسيفشل الاختبار. ثانيًا ، المستخدم الوهمي هو مثيل org.springframework.security.core.userdetails.User
class ، وهو التطبيق الداخلي لإطار Spring لواجهة UserDetails
، وإذا كان لدينا تطبيقنا الخاص ، فقد يتسبب ذلك في حدوث تعارضات لاحقًا ، أثناء تنفيذ اختبار.
إذا كانت العيوب السابقة عبارة عن مانعات لتطبيقنا ، فإن التعليق التوضيحي @WithUserDetails
هو السبيل للذهاب. يتم استخدامه عندما يكون لدينا تطبيقات UserDetailsService
و UserDetails
المخصصة. يفترض أن المستخدم موجود ، لذلك يتعين علينا إما إنشاء الصف الفعلي في قاعدة البيانات أو توفير مثيل UserDetailsService
الوهمي قبل تشغيل الاختبارات.
هذه هي الطريقة التي يمكننا بها استخدام هذا التعليق التوضيحي:
@Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }
هذا هو التعليق التوضيحي المفضل في اختبارات تكامل مشروعنا النموذجي لأن لدينا تطبيقات مخصصة للواجهات المذكورة أعلاه.
يسمح استخدام @WithAnonymousUser
كمستخدم مجهول. يكون هذا مناسبًا بشكل خاص عندما ترغب في إجراء معظم الاختبارات مع مستخدم معين ولكن مع بعض الاختبارات كمستخدم مجهول. على سبيل المثال ، سيجري ما يلي اختبارا 1 و test2 مع مستخدم وهمي و test3 مع مستخدم مجهول:
@SpringBootTest @AutoConfigureMockMvc @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void test1() { // Details omitted for brevity } @Test public void test2() { // Details omitted for brevity } @Test @WithAnonymousUser public void test3() throws Exception { // Details omitted for brevity } }
تغليف
في النهاية ، أود أن أذكر أن إطار عمل Spring Security ربما لن يفوز بأي مسابقة جمال ولديه بالتأكيد منحنى تعليمي حاد. لقد واجهت العديد من المواقف حيث تم استبدالها ببعض الحلول المحلية نظرًا لتعقيد تكوينها الأولي. ولكن بمجرد أن يفهم المطورون عناصره الداخلية ويتمكنون من إعداد التكوين الأولي ، يصبح استخدامه سهلًا نسبيًا.
في هذه المقالة ، حاولت توضيح كل التفاصيل الدقيقة للتكوين ، وآمل أن تجد الأمثلة مفيدة. للحصول على أمثلة التعليمات البرمجية الكاملة ، يرجى الرجوع إلى مستودع Git لنموذج مشروع Spring Security الخاص بي.