19. تأمين الوصول إلى خدمة ويب باستخدام Spring Security
19.1. دور Spring Security في تطبيق الويب
دعونا نضع Spring Security في سياق تطوير تطبيق ويب. في أغلب الأحيان، سيتم بناؤه على بنية متعددة الطبقات مثل ما يلي:
![]() |
- تمنح طبقة [Spring Security] حق الوصول إلى طبقة [الويب] للمستخدمين المصرح لهم فقط.
19.2. دليل تعليمي حول Spring Security
سنقوم مرة أخرى باستيراد دليل Spring باتباع الخطوات من 1 إلى 3 أدناه:
![]() |
![]() |
يتألف المشروع من العناصر التالية:
- في مجلد [templates]، ستجد صفحات HTML الخاصة بالمشروع؛
- [Application]: هي الفئة القابلة للتنفيذ للمشروع؛
- [MvcConfig]: هي فئة تكوين Spring MVC؛
- [WebSecurityConfig]: هي فئة تكوين Spring Security؛
19.2.1. تكوين Maven
المشروع [3] هو مشروع Maven. دعونا نلقي نظرة على ملف [pom.xml] الخاص به لنرى تبعياته:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-securing-web</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- tag::security[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- end::security[] -->
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- الأسطر 10–14: المشروع هو مشروع Spring Boot؛
- الأسطر 17–20: التبعية لإطار عمل [Thymeleaf]؛
- الأسطر 22–25: التبعية لإطار عمل Spring Security؛
19.2.2. طرق عرض Thymeleaf
![]() |
تبدو طريقة عرض [home.html] كما يلي:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- السطر 12: ستقوم السمة [th:href="@{/hello}"] بإنشاء السمة [href] لعلامة <a>. وستقوم القيمة [@{/hello}] بإنشاء المسار [<context>/hello]، حيث [context] هو سياق تطبيق الويب؛
فيما يلي كود HTML الذي تم إنشاؤه:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
تبدو طريقة العرض [hello.html] كما يلي:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- السطر 9: ستقوم السمة [th:inline="text"] بإنشاء نص علامة <h1>. يحتوي هذا النص على تعبير $ يجب تقييمه. العنصر [[${#httpServletRequest.remoteUser}]] هو قيمة السمة [RemoteUser] لطلب HTTP الحالي. هذا هو اسم المستخدم الذي قام بتسجيل الدخول؛
- السطر 10: نموذج HTML. ستقوم السمة [th:action="@{/logout}"] بإنشاء السمة [action] لعلامة [form]. ستقوم القيمة [@{/logout}] بإنشاء المسار [<context>/logout]، حيث [context] هو سياق تطبيق الويب؛
الرمز HTML الذي تم إنشاؤه هو كما يلي:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello user!</h1>
<form method="post" action="/logout">
<input type="submit" value="Sign Out" />
<input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
</form>
</body>
</html>
- السطر 8: ترجمة "Hello [[${#httpServletRequest.remoteUser}]]!"؛
- السطر 9: ترجمة @{/logout};
- السطر 11: حقل مخفي باسم (سمة الاسم) _csrf؛
الطريقة النهائية [login.html] هي كما يلي:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- السطر 9: تضمن السمة [th:if="${param.error}"] أن علامة <div> لن يتم إنشاؤها إلا إذا كان عنوان URL الذي يعرض صفحة تسجيل الدخول يحتوي على المعلمة [error] (http://context/login?error)؛
- السطر 10: تضمن السمة [th:if="${param.logout}"] أن علامة <div> لن يتم إنشاؤها إلا إذا كان عنوان URL الذي يعرض صفحة تسجيل الدخول يحتوي على المعلمة [logout] (http://context/login?logout)؛
- الأسطر 11–23: نموذج HTML؛
- السطر 11: سيتم إرسال النموذج إلى عنوان URL [<context>/login]، حيث <context> هو سياق تطبيق الويب؛
- السطر 13: حقل إدخال باسم [username]؛
- السطر 17: حقل إدخال باسم [password]؛
رمز HTML الذي تم إنشاؤه هو كما يلي:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div>
You have been logged out.
</div>
<form method="post" action="/login">
<div>
<label>
User Name :
<input type="text" name="username" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
<input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
</form>
</body>
</html>
لاحظ في السطر 28 أن Thymeleaf قد أضاف حقلًا مخفيًا باسم [_csrf].
19.2.3. تكوين Spring MVC
![]() |
تقوم فئة [MvcConfig] بتكوين إطار عمل Spring MVC:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- السطر 7: تجعل العلامة [@Configuration] فئة [MvcConfig] فئة تكوين؛
- السطر 8: تمتد فئة [MvcConfig] إلى فئة [WebMvcConfigurerAdapter] لتجاوز طرق معينة؛
- السطر 10: إعادة تعريف طريقة من الفئة الأصلية؛
- الأسطر 11–16: تسمح الطريقة [addViewControllers] بربط عناوين URL بعروض HTML. يتم إجراء الروابط التالية هناك:
عرض | |
/templates/home.html | |
/القوالب/hello.html | |
/القوالب/تسجيل_الدخول.html |
اللاحقة [html] والمجلد [templates] هما القيمتان الافتراضيتان اللتان يستخدمهما Thymeleaf. ويمكن تغييرهما عبر التكوين. يجب أن يكون المجلد [templates] موجودًا في جذر مسار فئات المشروع:
![]() |
في [1] أعلاه، مجلدا [java] و[resources] كلاهما مجلدان للمصدر. وهذا يعني أن محتوياتهما ستكون في جذر مسار فئات المشروع. وبالتالي، في [2]، سيكون مجلدا [hello] و[templates] في جذر مسار فئات المشروع.
19.2.4. تكوين Spring Security
![]() |
تقوم فئة [WebSecurityConfig] بتكوين إطار عمل Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- السطر 9: تجعل العلامة [@Configuration] فئة [WebSecurityConfig] فئة تكوين؛
- السطر 10: تجعل العلامة [@EnableWebSecurity] فئة [WebSecurityConfig] فئة تكوين Spring Security؛
- السطر 11: تمتد فئة [WebSecurity] إلى فئة [WebSecurityConfigurerAdapter] لتجاوز طرق معينة؛
- السطر 12: إعادة تعريف طريقة من الفئة الأصلية؛
- الأسطر 13-16: يتم تجاوز الطريقة [configure(HttpSecurity http)] لتعريف حقوق الوصول لمختلف عناوين URL الخاصة بالتطبيق؛
- السطر 14: تسمح الطريقة [http.authorizeRequests()] بربط عناوين URL بحقوق الوصول. يتم إجراء الروابط التالية هناك:
القاعدة | رمز | |
الوصول بدون مصادقة | | |
الوصول المصادق عليه فقط |
- السطر 15: يحدد طريقة المصادقة. تتم المصادقة عبر نموذج URL [/login] متاح للجميع [http.formLogin().loginPage("/login").permitAll()]. كما أن تسجيل الخروج متاح للجميع؛
- الأسطر 19-21: تعيد تعريف الطريقة [configure(AuthenticationManagerBuilder auth)] التي تدير المستخدمين؛
- السطر 20: تتم المصادقة باستخدام مستخدمين مبرمجين [auth.inMemoryAuthentication()]. يتم تعريف المستخدم هنا باستخدام اسم المستخدم [user] وكلمة المرور [password] والدور [USER]. يمكن منح المستخدمين الذين لديهم نفس الدور نفس الأذونات؛
19.2.5. فئة قابلة للتنفيذ
![]() |
فئة [Application] هي كما يلي:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- السطر 8: تعليمة [@EnableAutoConfiguration] توجه Spring Boot (السطر 3) لتنفيذ التكوين الذي لم يقم المطور بإعداده صراحةً؛
- السطر 9: يجعل فئة [Application] فئة تكوين Spring؛
- السطر 10: يوجه النظام إلى فحص الدليل الذي يحتوي على فئة [Application] للبحث عن مكونات Spring. وبالتالي سيتم اكتشاف الفئتين [MvcConfig] و [WebSecurityConfig] لأنهما تحتويان على تعليق [@Configuration]؛
- السطر 13: الطريقة [main] للفئة القابلة للتنفيذ؛
- السطر 14: يتم تنفيذ الطريقة الثابتة [SpringApplication.run] باستخدام فئة التكوين [Application] كمعلمة. لقد صادفنا هذه العملية من قبل ونعلم أن خادم Tomcat المضمن في تبعيات Maven للمشروع سيتم تشغيله ونشر المشروع عليه. رأينا أن أربعة عناوين URL تمت معالجتها [/, /home, /login, /hello] وأن بعضها محمي بحقوق الوصول.
19.2.6. اختبار التطبيق
لنبدأ بطلب عنوان URL [/]، وهو أحد عناوين URL الأربعة المقبولة. وهو مرتبط بالعرض [/templates/home.html]:
![]() |
عنوان URL المطلوب [/] متاح للجميع. ولهذا السبب تمكنا من استرداده. الرابط [هنا] هو كما يلي:
سيتم طلب عنوان URL [/hello] عند النقر على الرابط. هذا العنوان محمي:
القاعدة | رمز | |
الوصول بدون مصادقة | | |
الوصول المصادق عليه فقط |
يجب أن تكون مصادقًا عليه للوصول إليه. سيقوم Spring Security بعد ذلك بإعادة توجيه متصفح العميل إلى صفحة المصادقة. استنادًا إلى التكوين الموضح، هذه هي الصفحة الموجودة على عنوان URL [/login]. هذه الصفحة متاحة للجميع:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
لذا نحصل على [1]:
![]() |
فيما يلي شفرة المصدر للصفحة التي تم الحصول عليها:
- في السطر 7، يظهر حقل مخفي غير موجود في الصفحة الأصلية [login.html]. أضافه Thymeleaf. تم تصميم هذا الرمز، المعروف باسم CSRF (تزوير الطلبات عبر المواقع)، لمعالجة ثغرة أمنية. يجب إرسال هذا الرمز إلى Spring Security مع بيانات اعتماد المصادقة حتى يتم قبول المصادقة؛
نتذكر أن Spring Security لا يتعرف إلا على زوج المستخدم/كلمة المرور. إذا أدخلنا شيئًا آخر في [2]، فسنحصل على نفس الصفحة مع رسالة خطأ في [3]. قام Spring Security بإعادة توجيه المتصفح إلى عنوان URL [http://localhost:8080/login?error]. أدى وجود المعلمة [error] إلى عرض العلامة:
<div th:if="${param.error}">Invalid username and password.</div>
الآن، دعونا ندخل قيم اسم المستخدم وكلمة المرور المتوقعة [4]:
![]() |
- في [4]، نقوم بتسجيل الدخول؛
- في [5]، يقوم Spring Security بإعادة توجيهنا إلى عنوان URL [/hello] لأن هذا هو عنوان URL الذي طلبناه عندما تمت إعادة توجيهنا إلى صفحة تسجيل الدخول. تم عرض هوية المستخدم من خلال السطر التالي من [hello.html]:
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
تعرض الصفحة [5] النموذج التالي:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
عند النقر على زر [تسجيل الخروج]، يتم إرسال طلب POST إلى عنوان URL [/logout]. ومثل عنوان URL [/login]، فإن هذا العنوان متاح للجميع:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
في تعيين عناوين URL/العرض لدينا، لم نحدد أي شيء لعنوان URL [/logout]. ماذا سيحدث؟ دعونا نجرب ذلك:
![]() |
- في [6]، نضغط على زر [Sign Out]؛
- في [7]، نرى أنه تمت إعادة توجيهنا إلى عنوان URL [http://localhost:8080/login?logout]. طلب Spring Security إعادة التوجيه هذه. تسبب وجود المعلمة [logout] في عنوان URL في عرض السطر التالي في العرض:
<div th:if="${param.logout}">You have been logged out.</div>
19.2.7. الخلاصة
في المثال السابق، كان بإمكاننا كتابة تطبيق الويب أولاً ثم تأمينه. Spring Security غير تدخلي. يمكنك تنفيذ الأمان لتطبيق ويب تمت كتابته بالفعل. علاوة على ذلك، اكتشفنا النقاط التالية:
- من الممكن تعريف صفحة مصادقة؛
- يجب أن تكون المصادقة مصحوبة برمز CSRF الصادر عن Spring Security؛
- في حالة فشل المصادقة، يتم إعادة توجيهك إلى صفحة المصادقة مع معلمة خطأ إضافية في عنوان URL؛
- إذا نجحت المصادقة، يتم إعادة توجيهك إلى الصفحة المطلوبة وقت المصادقة. إذا طلبت صفحة المصادقة مباشرةً دون المرور بصفحة وسيطة، فإن Spring Security يعيد توجيهك إلى عنوان URL [/] (لم يتم توضيح هذه الحالة)؛
- يمكنك تسجيل الخروج عن طريق طلب عنوان URL [/logout] باستخدام طلب POST. ثم يقوم Spring Security بإعادة توجيهك إلى صفحة المصادقة مع معلمة "logout" في عنوان URL؛
تستند جميع هذه الاستنتاجات إلى السلوك الافتراضي لـ Spring Security. ويمكن تغيير هذا السلوك من خلال التهيئة عن طريق تجاوز بعض الطرق في فئة [WebSecurityConfigurerAdapter].
لن يكون الدرس السابق مفيدًا لنا كثيرًا في المستقبل. سنستخدم، في الواقع، ما يلي:
- قاعدة بيانات لتخزين المستخدمين وكلمات مرورهم وأدوارهم؛
- المصادقة المستندة إلى رأس HTTP؛
هناك عدد قليل جدًا من الدروس المتاحة لما نريد القيام به هنا. الحل الذي سنقترحه هو مزيج من مقتطفات الكود الموجودة هنا وهناك.














