19. ایمنسازی دسترسی به یک سرویس وب با Spring Security
19.1. نقش Spring Security در یک برنامه وب
بیایید امنیت اسپرینگ را در زمینه توسعه برنامههای وب در نظر بگیریم. اغلب اوقات، چنین برنامهای بر روی یک معماری چندلایه مانند مورد زیر ساخته میشود:
![]() |
- لایه [Spring Security] تنها به کاربران مجاز دسترسی به لایه [web] را میدهد.
19.2. آموزشی در مورد Spring Security
ما بار دیگر با دنبال کردن مراحل ۱ تا ۳ زیر، یک راهنمای Spring را وارد میکنیم:
![]() |
![]() |
پروژه از اجزای زیر تشکیل شده است:
- در پوشه [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>
<!-- برچسب::امنیت[] -->
<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>
- خطوط ۱۰–۱۴: پروژه یک پروژه Spring Boot است؛
- خطوط 17–20: وابستگی به فریمورک [Thymeleaf]؛
- خطوط ۲۲–۲۵: وابستگی به چارچوب 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>
- خط ۱۲: ویژگی [th:href="@{/hello}"] ویژگی [href] را برای تگ تولید خواهد کرد. مقدار [@{/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}]]!
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- خط ۹: ویژگی [th:inline="text"] متن تگ را تولید میکند. این متن شامل یک عبارت $ است که باید ارزیابی شود. عنصر [[${#httpServletRequest.remoteUser}]] مقدار ویژگی [RemoteUser] از پرسوجوی جاری HTTP است. این نام کاربر واردشده است؛
- خط ۱۰: یک فرم 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>
- خط ۸: ترجمه Hello [categorie[2]]!;
- خط ۹: ترجمه @{/logout};
- خط ۱۱: یک فیلد مخفی با نام (ویژگی name) _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>
- خط ۱۰: ویژگی [th:if="${param.error}"] تضمین میکند که تگ تنها در صورتی تولید میشود که URL، که صفحه ورود را نمایش میدهد، شامل پارامتر [error] (http://context/login?error) باشد؛
- خط ۱۰: ویژگی [th:if="${param.logout}"] تضمین میکند که تگ تنها در صورتی تولید میشود که URL، که صفحه ورود را نمایش میدهد، شامل پارامتر [logout] (http://context/login?logout) باشد؛
- خطوط ۱۱–۲۳: یک فرم HTML؛
- خط ۱۱: فرم به URL [<context>/login] ارسال خواهد شد، که در آن <context> زمینهٔ وباپلیکیشن است؛
- خط ۱۳: یک فیلد ورودی به نام [username];
- خط ۱۷: یک فیلد ورودی با نام [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>
توجه کنید که در خط ۲۸، 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");
}
}
- خط ۷: آناوتیشن [@Configuration] کلاس [MvcConfig] را به عنوان یک کلاس پیکربندی تعیین میکند؛
- خط ۸: کلاس [MvcConfig] از کلاس [WebMvcConfigurerAdapter] ارث میبرد تا برخی متدها را بازنویسی کند؛
- خط ۱۰: بازتعریف یک متد از کلاس والد؛
- خطوط ۱۱–۱۶: متد [addViewControllers] به URL اجازه میدهد تا با ویوهای HTML مرتبط شود. ارتباطات زیر برقرار میشوند:
نما | |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
پسوند [html] و پوشه [templates] مقادیر پیشفرض مورد استفاده توسط Thymeleaf هستند. آنها را میتوان از طریق پیکربندی تغییر داد. پوشه [templates] باید در ریشه classpath پروژه قرار داشته باشد:
![]() |
در [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");
}
}
- خط ۹: آناوتیشن [@Configuration] کلاس [WebSecurityConfig] را بهعنوان یک کلاس پیکربندی تعیین میکند؛
- خط ۱۰: تگ [@EnableWebSecurity] کلاس [WebSecurityConfig] را بهعنوان کلاس پیکربندی Spring Security تعیین میکند؛
- خط ۱۱: کلاس [WebSecurity] از کلاس [WebSecurityConfigurerAdapter] ارث میبرد تا برخی متدها را بازنویسی کند؛
- خط ۱۲: یک متد از کلاس والد بازنویسی شده است؛
- خطوط ۱۳–۱۶: متد [configure(HttpSecurity http)] برای تعریف حقوق دسترسی به URLهای مختلف در برنامه، بازتعریف شده است؛
- خط ۱۴: متد [http.authorizeRequests()] اجازه میدهد URLها با حقوق دسترسی مرتبط شوند. ارتباطات زیر در اینجا برقرار میشوند:
قاعده | کد | |
دسترسی بدون احراز هویت | | |
دسترسی فقط با احراز هویت |
- خط ۱۵: روش احراز هویت را تعریف میکند. احراز هویت از طریق فرمی که برای همه قابل دسترسی است انجام میشود: URL [/login] [http.formLogin().loginPage("/login").permitAll()]. خروج (Logout) نیز برای همه قابل دسترسی است؛
- خطوط ۱۹–۲۱: روش [configure(AuthenticationManagerBuilder auth)] را که مدیریت کاربران را بر عهده دارد، مجدداً تعریف میکنند؛
- خط ۲۰: احراز هویت با استفاده از کاربران پیشتعریفشده [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);
}
}
- خط ۸: آناوتیشن [@EnableAutoConfiguration] به Spring Boot (خط ۳) دستور میدهد پیکربندیای را که توسعهدهنده بهطور صریح انجام نداده است، انجام دهد؛
- خط ۹: کلاس [Application] را به یک کلاس پیکربندی Spring تبدیل میکند؛
- خط ۱۰: به سیستم دستور میدهد تا دایرکتوری حاوی کلاس [Application] را برای جستجوی کامپوننتهای Spring اسکن کند. بنابراین دو کلاس [MvcConfig] و [WebSecurityConfig] کشف خواهند شد، زیرا دارای انوتیشن [@Configuration] هستند؛
- خط ۱۳: متد [main] از کلاس قابل اجرا؛
- خط ۱۴: متد استاتیک [SpringApplication.run] با کلاس پیکربندی [Application] بهعنوان پارامتر اجرا میشود. ما قبلاً با این فرایند مواجه شدهایم و میدانیم که سرور Tomcat که در وابستگیهای Maven پروژه گنجانده شده است، راهاندازی خواهد شد و پروژه روی آن مستقر میشود. ما دیدهایم که چهار نمونه از URL توسط [/, /home, /login, /hello] مدیریت میشدند و برخی از آنها توسط حقوق دسترسی محافظت میشدند.
19.2.6. آزمون برنامه
بیایید با درخواست URL [/] شروع کنیم که یکی از چهار نمونه پذیرفتهشده URL است. این با نمای [/templates/home.html] مرتبط است:
![]() |
URL و [/] درخواستی برای همه قابل دسترسی است. به همین دلیل ما توانستیم آن را به دست آوریم. لینک [here] به شرح زیر است:
URL و [/hello] هنگام کلیک شما روی لینک درخواست خواهند شد. این مورد محافظت شده است:
قانون | کد | |
دسترسی بدون احراز هویت | | |
دسترسی فقط با احراز هویت |
برای دسترسی به آن باید احراز هویت شوید. سپس Spring Security مرورگر مشتری را به صفحهٔ احراز هویت هدایت میکند. بر اساس پیکربندی نمایشدادهشده، این صفحه در URL [/login] قرار دارد. این صفحه برای همه قابل دسترسی است:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
بنابراین ما به [1] میرسیم:
![]() |
کد منبع صفحه حاصل به شرح زیر است:
- در خط ۷، یک فیلد مخفی ظاهر میشود که در صفحه اصلی [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] نمایش داده شد:
صفحه [5] فرم زیر را نمایش میدهد:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
وقتی دکمه [Sign Out] کلیک میشود، یک POST روی URL [/logout] انجام خواهد شد. این، مانند URL و [/login]، برای همه قابل دسترسی است:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
در انجمن URL / views ما، برای 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 همراه باشد؛
- اگر احراز هویت ناموفق باشد، کاربر به صفحه احراز هویت هدایت میشود، با یک پارامتر اضافی «error» در توکن URL؛
- اگر احراز هویت موفقیتآمیز باشد، شما به صفحهای که در زمان احراز هویت درخواست شده بود، هدایت میشوید. اگر صفحه احراز هویت مستقیماً بدون عبور از یک صفحه واسطه درخواست شود، Spring Security شما را به URL [/] هدایت میکند (این سناریو پوشش داده نشده است)؛
- شما با درخواست URL [/logout] با یک POST از سیستم خارج میشوید. سپس Spring Security ما را با پارامتر «logout» در URL به صفحه احراز هویت هدایت میکند؛
تمام این نتیجهگیریها بر اساس رفتار پیشفرض Spring Security هستند. این رفتار را میتوان از طریق پیکربندی با بازنویسی برخی متدهای کلاس [WebSecurityConfigurerAdapter] تغییر داد.
آموزش قبلی در ادامه کمک چندانی به ما نخواهد کرد. در واقع ما از موارد زیر استفاده خواهیم کرد:
- یک پایگاه داده برای ذخیره کاربران، رمزهای عبور و نقشهای آنها؛
- احراز هویت مبتنی بر هدر (HTTP);
آموزشهای نسبتاً کمی برای کاری که میخواهیم انجام دهیم در دسترس است. راهحلی که ارائه خواهیم داد، گردآوری کدهایی است که از اینجا و آنجا یافت شدهاند.














