Skip to content

16. [Cours]: ایمن‌سازی دسترسی به یک سرویس وب با Spring Security

واژگان کلیدی: معماری چندلایه، اسپرینگ، تزریق وابستگی، سرویس وب / امن jSON، کلاینت / سرور

16.1. Support

 

پروژه‌های این فصل را می‌توان در پوشه [support / chap-16] یافت. اسکریپت SQL برای ایجاد پایگاه داده مورد نیاز برای تست استفاده می‌شود.

16.2. نقش Spring Security در یک برنامه وب

بیایید نقش Spring Security را در توسعهٔ برنامه‌های وب بررسی کنیم. در بیشتر موارد، یک برنامهٔ وب بر روی معماری چندلایهٔ زیر ساخته می‌شود:

  • لایه [Spring Security] تنها به کاربران مجاز اجازه دسترسی به لایه [web] را می‌دهد.

16.3. آموزشی در مورد Spring Security

ما بار دیگر با دنبال کردن مراحل ۱ تا ۳ زیر، یک راهنمای Spring را وارد می‌کنیم:

  

پروژه از اجزای زیر تشکیل شده است:

  • در پوشه [templates]، صفحات پروژه HTML را خواهید یافت؛
  • [Application]: کلاس اجرایی پروژه است؛
  • [MvcConfig]: کلاس پیکربندی Spring MVC است؛
  • [WebSecurityConfig]: کلاس پیکربندی Spring Security است؛

16.3.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
  • خطوط 22–25: وابستگی به فریم‌ورک Spring Security؛

16.3.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] را برای تگ [<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}]]!
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • خط ۹: ویژگی [th:inline="text"] متن تگ [<h1>] را تولید می‌کند. این متن شامل یک عبارت $ است که باید ارزیابی شود. عنصر [[${#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 [document.forms[0]]!;
  • خط ۹: ترجمه @{/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];
  • خط 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>

توجه کنید که در خط ۲۸، Thymeleaf یک فیلد مخفی به نام [_csrf] اضافه کرده است.

16.3.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 مرتبط شود. ارتباطات زیر برقرار می‌شوند:
URL
نما
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

پسوند [html] و پوشه [templates] مقادیر پیش‌فرض مورد استفاده توسط Thymeleaf هستند. آنها را می‌توان از طریق پیکربندی تغییر داد. پوشه [templates] باید در ریشه کلاس‌پث پروژه قرار داشته باشد:

در [1]، پوشه‌های [java] و [resources] هر دو پوشه‌های منبع هستند. این بدان معناست که محتویات آن‌ها در ریشهٔ مسیر کلاس پروژه قرار خواهد گرفت. بنابراین، در داخل [2]، پوشه‌های [hello] و [templates] در ریشهٔ مسیر کلاس قرار خواهند گرفت.

16.3.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
قانون
کد
/, /home
دسترسی بدون احراز هویت

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
دسترسی فقط با احراز هویت
http.anyRequest().authenticated();
  • خط ۱۵: روش احراز هویت را تعریف می‌کند. احراز هویت از طریق فرمی که برای همه قابل دسترسی است انجام می‌شود: URL [/login] [http.formLogin().loginPage("/login").permitAll()]. خروج (Logout) نیز برای همه قابل دسترسی است؛
  • خطوط ۱۹–۲۱: روش [configure(AuthenticationManagerBuilder auth)] را که مدیریت کاربران را بر عهده دارد، مجدداً تعریف کنید؛
  • خط ۲۰: احراز هویت با استفاده از کاربران پیش‌تعریف‌شده [auth.inMemoryAuthentication()] انجام می‌شود. یک کاربر در اینجا با نام کاربری [user]، رمز عبور [password] و نقش [USER] تعریف می‌شود. می‌توان همان حقوق را به کاربرانی که دارای نقش یکسان هستند نیز اعطا کرد؛

16.3.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] مدیریت می‌شدند و برخی از آن‌ها توسط حقوق دسترسی محافظت می‌شدند.

16.3.6. آزمون برنامه

بیایید با درخواست URL [/] شروع کنیم که یکی از چهار نمونه پذیرفته‌شده URL است. این با نمای [/templates/home.html] مرتبط است:

 

URL و [/] درخواستی برای همه قابل دسترسی است. به همین دلیل ما توانستیم آن را به دست آوریم. لینک [here] به شرح زیر است:

Click <a href="/hello">here</a> to see a greeting.

URL و [/hello] هنگام کلیک شما روی لینک درخواست خواهند شد. این مورد محافظت شده است:

URL
قانون
کد
/, /home
دسترسی بدون احراز هویت

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
دسترسی فقط برای کاربران احراز هویت‌شده
http.anyRequest().authenticated();

برای دسترسی به آن باید احراز هویت شوید. سپس Spring Security مرورگر مشتری را به صفحهٔ احراز هویت هدایت می‌کند. بر اساس پیکربندی نمایش‌داده‌شده، این صفحه در URL [/login] قرار دارد. این صفحه برای همه قابل دسترسی است:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

بنابراین ما به [1] می‌رسیم:

کد منبع صفحه حاصل به شرح زیر است:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • در خط ۷، یک فیلد مخفی ظاهر می‌شود که در صفحهٔ اصلی [login.html] وجود ندارد. Thymeleaf آن را اضافه کرده است. این کد، که با نام CSRF (جعلی‌سازی درخواست بین‌سایتی) شناخته می‌شود، برای از بین بردن یک آسیب‌پذیری امنیتی طراحی شده است. این توکن باید به همراه احراز هویت به Spring Security بازگردانده شود تا پذیرفته شود؛

به یاد داریم که تنها کاربر user/password توسط 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}]]!

صفحه [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>

16.3.7. نتیجه‌گیری

در مثال قبلی، می‌توانستیم ابتدا وب‌اپلیکیشن را بنویسیم و سپس آن را ایمن کنیم. Spring Security غیرمداخله گر است. امکان پیاده‌سازی امنیت برای یک وب‌اپلیکیشن که قبلاً نوشته شده است، وجود دارد. علاوه بر این، ما به نکات زیر پی بردیم:

  • امکان تعریف یک صفحه احراز هویت وجود دارد؛
  • احراز هویت باید همراه با توکن CSRF صادر شده توسط Spring Security باشد؛
  • اگر احراز هویت ناموفق باشد، کاربر به صفحه احراز هویت هدایت می‌شود، با یک پارامتر اضافی «error» در توکن URL؛
  • اگر احراز هویت موفقیت‌آمیز باشد، شما به صفحه‌ای که در زمان احراز هویت درخواست شده بود، هدایت می‌شوید. اگر صفحه احراز هویت مستقیماً بدون عبور از یک صفحه واسطه درخواست شود، Spring Security شما را به URL [/] هدایت می‌کند (این سناریو پوشش داده نشده است)؛
  • شما با درخواست URL [/logout] با یک POST از سیستم خارج می‌شوید. سپس Spring Security ما را با پارامتر 'logout' در URL به صفحه احراز هویت هدایت می‌کند؛

تمام این نتیجه‌گیری‌ها بر اساس رفتار پیش‌فرض Spring Security است. این رفتار را می‌توان از طریق پیکربندی با override کردن برخی متدهای کلاس [WebSecurityConfigurerAdapter] تغییر داد.

آموزش قبلی در ادامه کمک چندانی به ما نخواهد کرد. در واقع ما از موارد زیر استفاده خواهیم کرد:

  • یک پایگاه داده برای ذخیره کاربران، رمزهای عبور و نقش‌های آن‌ها؛
  • احراز هویت مبتنی بر هدر HTTP؛

آموزش‌های نسبتاً کمی برای کاری که می‌خواهیم انجام دهیم در دسترس است. راه‌حلی که ارائه خواهیم داد، گردآوری کدهایی است که از اینجا و آنجا یافت شده‌اند.

16.4. راه‌اندازی امنیت برای سرویس وب / داده‌های محصول JSON

16.4.1. پایگاه داده

پایگاه داده [dbintrospringdata] برای ثبت کاربران، رمزهای عبور و نقش‌های آن‌ها به‌روزرسانی می‌شود. سه جدول جدید اضافه شده‌اند:

Image

جدول [USERS]: کاربران

  • ID: کلید اصلی؛
  • VERSION: ستون نسخه‌بندی ردیف؛
  • IDENTITY: شناسه توصیفی برای کاربر؛
  • LOGIN: نام کاربری کاربر؛
  • PASSWORD: رمز عبور آنها؛

در جدول USERS، رمزهای عبور به صورت متن ساده ذخیره نمی‌شوند:

 

الگوریتم مورد استفاده برای رمزگذاری گذرواژه‌ها، الگوریتم BCRYPT است.

جدول [ROLES]: نقش‌ها

  • ID: کلید اصلی؛
  • VERSION: ستون نسخه‌بندی ردیف؛
  • NAME: نام نقش. به طور پیش‌فرض، Spring Security انتظار نام‌هایی در قالب ROLE_XX را دارد، برای مثال ROLE_ADMIN یا ROLE_GUEST;
 

جدول [USERS_ROLES]: جدول الحاق برای USERS / ROLES

یک کاربر ممکن است چندین نقش داشته باشد و یک نقش ممکن است شامل چندین کاربر باشد. این یک رابطهٔ چندبه‌چند است که توسط جدول [USERS_ROLES] نمایش داده می‌شود.

  • ID: کلید اصلی؛
  • VERSION: ستون نسخه‌بندی ردیف؛
  • USER_ID: شناسه کاربر؛
  • ROLE_ID: شناسه نقش؛
 

16.4.2. پروژه اکلیپس

ما پروژهٔ Eclipse زیر را ایجاد می‌کنیم:

1
  

  • در [1]: پروژه جدید حاوی بسته‌های زیر:
    • [spring.security.entities]: حاوی اشیاء JPA متناظر با سه جدول جدید پایگاه داده است؛
    • [spring.security.repositories]: حاوی فایل‌های Spring Data [repositories] مرتبط با سه جدول جدید است؛
    • [spring.security.dao]: شامل سرویسی بر اساس فایل‌های [repositories] است؛
    • [spring.security.config]: شامل پیکربندی پروژه، از جمله پیکربندی دسترسی امن به سرویس وب است؛
    • [spring.security.boot]: شامل کلاس راه‌اندازی سرویس وب امن است؛

16.4.3. پیکربندی Maven

پروژه جدید یک پروژه Maven است که توسط فایل زیر پیکربندی شده است: [pom.xml]:


<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>istia.st.spring.security</groupId>
    <artifactId>intro-spring-security-server-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>intro-spring-security-server-01</name>
    <description>démo spring security</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>istia.st.webjson</groupId>
            <artifactId>intro-server-webjson-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- امنیت اسپرینگ -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- لاگ‌های Spring -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- آزمون Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- پلاگین‌ها -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • خطوط ۲۳–۲۷: ما پیکربندی موجود را با آرشیو وب سرویس / JSON مورد بحث مجدداً استفاده می‌کنیم؛
  • خطوط ۲۹–۳۲: وابستگی‌ای که کلاس‌های Spring Security را فراهم می‌کند؛
  • خطوط ۳۴–۳۷: کتابخانهٔ لاگ‌گیری؛
  • خطوط ۳۹–۴۲: کتابخانه‌ای که امکان استفاده از anotationهای Spring Boot را فراهم می‌کند؛
  • خطوط ۴۴–۴۸: کتابخانه مورد نیاز برای تست؛

16.4.4. انتیتی‌های جدید [JPA]

لایه JPA سه موجودیت جدید را تعریف می‌کند:

  

کلاس [User] تصویر جدول [USERS] است:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {

    // ویژگی‌ها
    @Column(name = "NAME")
    private String name;
    @Column(name = "LOGIN")
    private String login;
    @Column(name = "PASSWORD")
    private String password;

    // سازنده
    public User() {
    }

    public User(String name, String login, String password) {
        this.name = name;
        this.login = login;
        this.password = password;
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...
}
  • خط ۱۱: کلاس، کلاس [AbstractEntity] را که پیش از این برای سایر اشیاء استفاده شده بود، گسترش می‌دهد؛

کلاس [Role] تصویر جدول [ROLES] است:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {

    // ویژگی‌ها
    @Column(name="NAME")
    private String name;

    // سازنده‌ها
    public Role() {
    }

    public Role(String name) {
        this.name = name;
    }

    // گیرنده و تنظیم‌کننده
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

کلاس [UserRole] آینه‌ی جدول [USERS_ROLES] است:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {

    // کلیدهای خارجی
    @Column(name = "USER_ID", insertable = false, updatable = false)
    private Long userId;
    @Column(name = "ROLE_ID", insertable = false, updatable = false)
    private Long roleId;

    // یک UserRole به یک User ارجاع می‌دهد
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;

    // یک UserRole به یک Role ارجاع می‌دهد
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // سازنده‌ها
    public UserRole() {

    }

    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...
}

  • سطور ۲۲–۲۴: کلید خارجی را از جدول [USERS_ROLES] به جدول [USERS] تعریف کنید؛
  • خطوط ۲۷–۲۹: تعریف کلید خارجی از جدول [USERS_ROLES] به جدول [ROLES];

16.4.5. [repositories]

هر یک از اشیاء JPA پیشین توسط Spring Data [repository] مدیریت می‌شود:

  

رابط [UserRepository] دسترسی به موجودیت‌های [User] را مدیریت می‌کند:


package spring.security.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;
import spring.security.entities.User;

public interface UserRepository extends CrudRepository<User, Long> {

    // فهرست نقش‌ها برای کاربری که با شناسهٔ خود شناسایی شده است
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    //فهرست نقش‌ها برای کاربری که با ورود نام کاربری منحصربه‌فردش شناسایی شده است
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    //جستجو برای کاربر بر اساس نام کاربری
    User findUserByLogin(String login);
}
  • خط ۹: رابط [UserRepository] رابط Spring Data [CrudRepository] (خط ۴) را گسترش می‌دهد؛
  • خطوط ۱۲–۱۳: متد [getRoles(User user)] تمام نقش‌های یک کاربر را که با [id] شناسایی شده است، بازیابی می‌کند
  • خطوط ۱۶–۱۷: مشابه مورد بالا، اما برای کاربری که با نام کاربری و رمز عبور شناسایی می‌شود؛
  • خط ۲۰: برای یافتن کاربر با نام کاربری او؛

رابط [RoleRepository] دسترسی به اشیاء [Role] را مدیریت می‌کند:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;

public interface RoleRepository extends CrudRepository<Role, Long> {

    //جستجو برای یک نقش بر اساس نام
    Role findRoleByName(String name);

}
  • خط ۷: رابط [RoleRepository] رابط [CrudRepository] را گسترش می‌دهد؛
  • خط ۱۰: یک نقش را می‌توان با نام آن جستجو کرد؛

رابط [UserRoleRepository] دسترسی به اشیاء [UserRole] را مدیریت می‌کند:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.UserRole;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • خط ۵: رابط [UserRoleRepository] صرفاً رابط [CrudRepository] را بدون افزودن هیچ متد جدید گسترش می‌دهد؛

16.4.6. کلاس‌های مدیریت کاربر و نقش

  

Spring Security نیازمند ایجاد کلاسی است که رابط [UsersDetail] زیر را پیاده‌سازی کند:

 

این رابط در اینجا توسط کلاس [AppUserDetails] پیاده‌سازی شده است:


package spring.security.dao;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // ویژگی‌ها
    private User user;
    private UserRepository userRepository;

    // سازنده‌ها
    public AppUserDetails() {
    }

    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }

    // -------------------------رابط
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLogin();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
    ...
}
  • خط ۱۴: کلاس [AppUserDetails] رابط [UserDetails] را پیاده‌سازی می‌کند؛
  • خطوط ۱۹–۲۰: این کلاس یک کاربر (خط ۱۹) و مخزنی را که جزئیات آن کاربر را فراهم می‌کند (خط ۲۰) در بر می‌گیرد؛
  • خطوط 26–29: سازنده‌ای که کلاس را با یک کاربر و مخزن او نمونه سازی می‌کند؛
  • خطوط ۳۲–۳۶: پیاده‌سازی متد [getAuthorities] از رابط [UserDetails]. این متد باید یک مجموعه از عناصر از نوع [GrantedAuthority] یا یک نوع مشتق را ایجاد کند. در اینجا، ما از نوع مشتق [SimpleGrantedAuthority] (خط ۳۶) استفاده می‌کنیم که نام یکی از نقش‌های کاربر را از خط ۱۹ در خود جای داده است؛
  • خطوط ۳۵–۳۷: لیست نقش‌های کاربر در خط ۱۹ به صورت حلقه‌ای پردازش می‌شود تا لیستی از عناصر از نوع [SimpleGrantedAuthority] ساخته شود؛
  • خطوط ۴۲–۴۴: متد [getPassword] از رابط [UserDetails] پیاده‌سازی می‌شود. رمز عبور کاربر در خط ۱۹ بازگردانده می‌شود؛
  • خطوط ۴۲–۴۴: متد [getUserName] از رابط [UserDetails] پیاده‌سازی می‌شود. ورود کاربر از خط ۱۹ بازگردانده می‌شود؛
  • خطوط ۵۱–۵۴: حساب کاربری کاربر هرگز منقضی نمی‌شود؛
  • خطوط ۵۶–۵۹: حساب کاربری کاربر هرگز مسدود نمی‌شود؛
  • خطوط ۶۱–۶۴: اعتبارنامه‌های کاربر هرگز منقضی نمی‌شوند؛
  • خطوط ۶۶–۶۹: حساب کاربری کاربر همیشه فعال است؛

Spring Security همچنین نیازمند وجود کلاسی است که رابط [AppUserDetailsService] را پیاده‌سازی کند:

 

این رابط توسط کلاس زیر پیاده‌سازی شده است، [AppUserDetailsService]:


package spring.security.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import spring.security.entities.User;
import spring.security.repositories.UserRepository;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // جستجوی کاربر بر اساس نام کاربری
        User user = userRepository.findUserByLogin(login);
        // یافت شد؟
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // بازگرداندن جزئیات کاربر
        return new AppUserDetails(user, userRepository);
    }

}
  • خط ۱۲: این کلاس یک کامپوننت Spring خواهد بود و بنابراین در زمینهٔ آن در دسترس است؛
  • خطوط ۱۵–۱۶: کامپوننت [UserRepository] در اینجا تزریق خواهد شد؛
  • خطوط ۱۹–۲۸: پیاده‌سازی متد [loadUserByUsername] از رابط [UserDetailsService] (خط ۱۰). پارامتر، نام کاربری کاربر است؛
  • خط ۲۱: کاربر با استفاده از نام کاربری‌اش جستجو می‌شود؛
  • خطوط ۲۳–۲۵: اگر کاربر پیدا نشود، یک استثنا پرتاب می‌شود؛
  • خط ۲۷: یک شیء [AppUserDetails] ساخته و رندر می‌شود. این شیء در واقع از نوع [UserDetails] (خط ۱۹) است؛

16.4.7. پیکربندی پروژه

پروژه توسط دو کلاس پیکربندی شده است:

کلاس [DaoConfig] لایه [DAO] را که توسط پروژه جدید معرفی شده است، پیکربندی می‌کند:


package spring.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao" })
@Import({ spring.data.config.DaoConfig.class })
public class DaoConfig {

    // ثوابت
    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}
  • خط ۱۰: کلاس پیکربندی [spring.data.config.DaoConfig] از پروژه [intro-spring-data-01] وارد می‌شود، که لایه [DAO] را برای محصولات و دسته‌بندی‌ها پیاده‌سازی می‌کند؛
  • خط ۸: پوشه‌های حاوی Spring Data مربوط به [repositories] در پروژهٔ جاری مشخص شده‌اند؛
  • خط ۹: پوشه‌های موجود در پروژهٔ جاری را که حاوی کامپوننت‌های Spring مربوط به لایهٔ [DAO] هستند، مشخص می‌کند؛
  • خط 14: پوشه‌های حاوی اشیاء JPA را مشخص می‌کند. این‌ها شامل فایل‌های پروژه [intro-spring-data-01] و پروژه سرور امن هستند. این اطلاعات موضوع بین (bean) در خطوط 16–19 است. این بین، بین با نام مشابه در پروژه [intro-spring-data-01] را override می‌کند:

    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities" };
    
    //EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
}

در لایه [DAO]، خط ۸ پوشه‌های مشخص‌شده در خط ۱ را اسکن می‌کند. به دلیل تعریف مجدد بین (bean) در خطوط ۱۴–۱۷ در پروژه امن (lines 16–19)، خط ۸ بالا اکنون پوشه‌های ["spring.data.entities", "spring.security.entities"] را اسکن خواهد کرد. توجه داشته باشید که کلاسی که در خط ۱۰ از [spring.security.config.DaoConfig] وارد شده است باید شامل anotation [@Configuration] باشد؛ در غیر این صورت، رفتاری که به تازگی توضیح داده شد کار نخواهد کرد.

کلاس [SecurityConfig] جنبه امنیتی پروژه را پیکربندی می‌کند. ما قبلاً با یک کلاس پیکربندی 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");
    }
}

ما همان رویه را دنبال خواهیم کرد:

  • خط ۱۱: تعریف یک کلاس که از کلاس [WebSecurityConfigurerAdapter] ارث می‌برد؛
  • خط ۱۳: تعریف یک متد [configure(HttpSecurity http)] که حقوق دسترسی به متدهای مختلف URL سرویس وب را مشخص می‌کند؛
  • خط ۱۹: متد [configure(AuthenticationManagerBuilder auth)] را تعریف کنید که کاربران و نقش‌های آن‌ها را مشخص می‌کند؛

کلاس [SecurityConfig] به شرح زیر خواهد بود:


package spring.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import spring.security.dao.AppUserDetailsService;

@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.service" })
@Import({ spring.webjson.config.AppConfig.class, DaoConfig.class })
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AppUserDetailsService appUserDetailsService;

    //امنیت
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        //احراز هویت توسط bean [appUserDetailsService] انجام می‌شود
        // رمز عبور با استفاده از الگوریتم هش BCrypt رمزگذاری می‌شود
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // اپلیکیشن امن؟
        if (activateSecurity) {
            // رمز عبور از طریق هدر Authorisation: Basic xxxx ارسال می‌شود
            http.httpBasic();
            // روش HTTP OPTIONS باید برای همه مجاز باشد
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            //تنها نقش ADMIN می‌تواند از برنامه استفاده کند
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // تمام URL
                    .hasRole("ADMIN");
            // هیچ جلسه‌ای وجود ندارد
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • خط ۱۶: برای فعال‌سازی کامپوننت‌های Spring Security؛
  • خط ۱۷: افزودن کامپوننت‌های Spring از پکیج [spring.security.service];
  • خط ۱۸: بیین‌ها را از لایه [DAO] که همین حالا توصیف کردیم، و همچنین از وب‌سرور ناامن / jSON وارد کنید؛
  • خطوط ۲۱–۲۲: کلاس [AppUserDetails] که دسترسی به کاربران برنامه را فراهم می‌کند، تزریق می‌شود؛
  • خط ۲۵: یک بولین که وب‌اپلیکیشن را ایمن می‌کند (true) یا ایمن نمی‌کند (false
  • خطوط ۲۷–۳۲: متد [configure(HttpSecurity http)] کاربران و نقش‌های آن‌ها را تعریف می‌کند. این متد یک پارامتر از نوع [AuthenticationManagerBuilder] را می‌پذیرد. این پارامتر با دو مورد اطلاعات (خط ۳۸) غنی‌سازی می‌شود:
    • یک مرجع به سرویس [appUserDetailsService] در خط 22، که دسترسی به کاربران ثبت‌نام‌شده را فراهم می‌کند. باید توجه داشت که این موضوع صراحتاً بیان نشده است. بنابراین، آن‌ها می‌توانند در یک کش (cache)، توسط یک سرویس وب بازگردانده شوند و غیره ذخیره شوند.
    • نوع رمزنگاری است که برای رمز عبور استفاده می‌شود. باید توجه داشت که در اینجا از الگوریتم BCrypt استفاده شده است؛
  • خطوط 34–52: متد [configure(HttpSecurity http)] حقوق دسترسی به توکن‌های URL سرویس وب را تعریف می‌کند؛
  • خط ۳۷: همانطور که در پروژه مقدماتی دیدیم، Spring Security به طور پیش‌فرض یک توکن CSRF (Cross-Site Request Forgery) را مدیریت می‌کند که کاربر برای احراز هویت باید آن را به سرور بازگرداند. در اینجا، این مکانیزم غیرفعال شده است. این موضوع، در ترکیب با تنظیم بولی (isSecured=false)، به وب‌اپلیکیشن اجازه می‌دهد تا بدون امنیت استفاده شود؛
  • خط ۴۱: ما احراز هویت را از طریق هدر HTTP فعال می‌کنیم. کلاینت باید هدر زیر را ارسال کند:
Authorization:Basic code

که در آن «code» رمزگذاری Base64 رشته «login:password» است. به عنوان مثال، رمزگذاری Base64 رشته «admin:admin» برابر با «YWRtaW46YWRtaW4=» است. بنابراین، کاربری با نام کاربری [admin] و رمز عبور [admin]، برای احراز هویت، سربرگ زیر را ارسال خواهد کرد: HTTP

Authorization:Basic YWRtaW46YWRtaW4=
  • خطوط ۴۶–۴۸: نشان می‌دهند که تمام منابع URL سرویس وب برای کاربرانی با نقش [ROLE_ADMIN] قابل دسترسی هستند. این بدان معناست که کاربری بدون این نقش نمی‌تواند به سرویس وب دسترسی پیدا کند؛
  • خط ۵۰: در حالت [session]، کاربری که یک‌بار احراز هویت کرده است، برای دسترسی‌های بعدی نیازی به انجام مجدد آن ندارد. در اینجا این حالت غیرفعال است، بنابراین کاربر باید هر بار که به سیستم دسترسی پیدا می‌کند، احراز هویت کند؛

16.4.8. آزمایش‌های لایه [DAO]

  

ابتدا، یک کلاس قابل اجرا [CreateUser] ایجاد می‌کنیم که قادر به ایجاد یک کاربر با نقش است:


package sprin.security.tests;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;

import spring.security.config.DaoConfig;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.entities.UserRole;
import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;

public class CreateUser {

    public static void main(String[] args) {
        // دستور: login password roleName

        //سه پارامتر لازم است
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        //بازیابی پارامترها
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // زمینهٔ Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        //آیا نقش از قبل وجود دارد؟
        Role role = roleRepository.findRoleByName(roleName);
        // اگر وجود ندارد، آن را ایجاد کنید
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        //آیا کاربر از قبل وجود دارد؟
        User user = userRepository.findUserByLogin(login);
        // اگر نه، آن را ایجاد کنید
        if (user == null) {
            // رمز عبور را با bcrypt هش کنید
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // کاربر را ذخیره کنید
            user = userRepository.save(new User(login, login, crypt));
            // ما رابطه را با نقش ایجاد می‌کنیم
            userRoleRepository.save(new UserRole(user, role));
        } else {
            //کاربر از قبل وجود دارد – آیا او نقش درخواستی را دارد؟
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            //اگر پیدا نشد، رابطه با نقش ایجاد شود
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        //بستن کانکست Spring
        context.close();
        // پایان
        System.out.println("Travail terminé...");
    }

}
  • خط ۱۷: کلاس منتظر سه آرگومان است که یک کاربر را تعریف می‌کنند: نام کاربری، رمز عبور و نقش؛
  • خطوط ۲۵–۲۷: سه پارامتر بازیابی می‌شوند؛
  • خط ۲۹: کانکست Spring از کلاس پیکربندی [AppConfig] ساخته می‌شود؛
  • خطوط ۳۰–۳۲: ارجاعات به سه نمونه از کلاس [Repository] که ممکن است برای ایجاد کاربر مفید باشند، بازیابی می‌شوند؛
  • خط ۳۴: بررسی می‌کنیم که آیا نقش از قبل وجود دارد یا خیر؛
  • خطوط ۳۶–۳۸: اگر وجود نداشته باشد، آن را در پایگاه داده ایجاد می‌کنیم. نام آن به شکل [ROLE_XX] خواهد بود؛
  • خط ۴۰: بررسی می‌کنیم که آیا نام کاربری قبلاً وجود دارد یا خیر؛
  • خطوط ۴۲–۴۹: اگر نام کاربری وجود نداشته باشد، آن را در پایگاه داده ایجاد می‌کنیم؛
  • خط ۴۴: رمز عبور رمزگذاری می‌شود. در اینجا، ما از کلاس Spring Security به نام [BCrypt] (خط ۴) استفاده می‌کنیم. بنابراین به آرشیوهای این فریم‌ورک نیاز داریم. فایل [pom.xml] این وابستگی را شامل می‌شود:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • خط ۴۶: کاربر در پایگاه داده ذخیره می‌شود؛
  • خط ۴۸: همان‌طور که رابطه آن‌ها را به نقش‌شان پیوند می‌دهد؛
  • خطوط ۵۱–۵۷: اگر ورود به سیستم از قبل وجود داشته باشد – سپس بررسی می‌کنیم که آیا نقشی که می‌خواهیم به آنها اختصاص دهیم از قبل در میان نقش‌هایشان وجود دارد یا خیر؛
  • خطوط ۵۹–۶۱: اگر نقش مورد جستجو یافت نشود، یک سطر در جدول [USERS_ROLES] برای مرتبط کردن کاربر با نقشش ایجاد می‌شود؛
  • برای استثناهای احتمالی هیچ تمهیدی در نظر گرفته نشده است. این یک کلاس کمکی برای ایجاد سریع یک کاربر با نقش است.

وقتی کلاس با آرگومان‌های [x x guest] اجرا می‌شود، نتایج زیر در پایگاه داده به دست می‌آیند:

جدول [USERS]

جدول [ROLES]

 

جدول [USERS_ROLES]

 

اکنون بیایید کلاس دوم، [UsersTest]، را که آزمونی برای JUnit است، بررسی کنیم:

  

package spring.security.tests;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.security.config.DaoConfig;
import spring.security.dao.AppUserDetails;
import spring.security.dao.AppUserDetailsService;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // نقشه‌کش jSON
    private ObjectMapper mapper = new ObjectMapper();

    @Test
    public void findAllUsersWithTheirRoles() throws JsonProcessingException {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(String.format("\n----------Utilisateur [%s]",mapper.writeValueAsString(user)));
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }

    @Test
    public void findUserByLogin() {
        //بازیابی کاربر [admin]
        User user = userRepository.findUserByLogin("admin");
        //بررسی کنید که رمز عبور آنها [admin] است
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // ما نقش مدیر / مدیر را بررسی می‌کنیم
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        //بازیابی کاربر [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        //ما بررسی می‌کنیم که رمز عبور آن‌ها [admin] است
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        //بررسی نقش مدیر / مدیر
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // روش ابزار – موارد موجود در یک مجموعه را نمایش می‌دهد
    private void display(String message, Iterable<?> elements) throws JsonProcessingException {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(mapper.writeValueAsString(element));
        }
    }
}
  • خطوط ۳۷–۴۴: آزمون بصری. تمام کاربران به همراه نقش‌هایشان نمایش داده می‌شوند؛
  • خطوط ۴۶–۵۶: ما بررسی می‌کنیم که کاربر [admin] رمز عبور [admin] و نقش [ROLE_ADMIN] را با استفاده از مخزن [UserRepository] دارد؛
  • خط ۵۱: [admin] رمز عبور متن ساده است. در پایگاه داده، با استفاده از الگوریتم BCrypt رمزگذاری شده است. روش [BCrypt.checkpw] برای تأیید اینکه گذرواژهٔ متن ساده، پس از رمزگذاری، واقعاً با گذرواژهٔ موجود در پایگاه داده یکسان است، استفاده می‌شود؛
  • خطوط ۵۸–۶۹: ما تأیید می‌کنیم که کاربر [admin] رمز عبور [admin] و نقش [ROLE_ADMIN] را با استفاده از سرویس [appUserDetailsService] دارد؛

تست‌ها با لاگ‌های زیر با موفقیت اجرا شدند:

----------Utilisateur [{"id":14,"version":0,"identity":"admin","login":"admin","password":"$2a$10$FN1LMKjPU46aPffh9Zaw4exJOLo51JJPWrxqzak/eJrbt3CO9WzVG"}]
Roles :
{"id":6,"version":0,"name":"ROLE_ADMIN"}

----------Utilisateur [{"id":15,"version":0,"identity":"user","login":"user","password":"$2a$10$SJehR9Mv2VdyRZo9F0rXa.hKAoGLhJg6kSdyfExi40mEJrNOj0BTq"}]
Roles :
{"id":7,"version":0,"name":"ROLE_USER"}

----------Utilisateur [{"id":16,"version":0,"identity":"guest","login":"guest","password":"$2a$10$ubyWJb/vg2XZnUOAUjspZuz9jpHP3fIbPTbwQU115EtLdeSZ2PB7q"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

----------Utilisateur [{"id":17,"version":0,"identity":"x","login":"x","password":"$2a$10$kEXA56wpKHFReVqwQTyWguKguK8I4uhA2zb6t3wGxag8Dyv7AhLom"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

16.4.9. تست‌های سرویس وب

ما سرویس وب را با استفاده از کلاینت کروم [Advanced Rest Client] آزمایش خواهیم کرد. لازم است هدر احراز هویت HTTP را مشخص کنیم:

Authorization:Basic code

که در آن [code] رشته رمزگذاری‌شده با Base64، [login:password] است. برای تولید این کد، می‌توانید از برنامه زیر استفاده کنید:

  

package spring.security.helpers;

import org.springframework.security.crypto.codec.Base64;

public class Base64Encoder {

    public static void main(String[] args) {
        // دو آرگومان را انتظار دارد: نام کاربری و رمز عبور
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // دو آرگومان را بازیابی می‌کند
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // رشته را رمزگذاری می‌کند
        byte[] data = Base64.encode(chaîne.getBytes());
        //رمزگذاری Base64 آن را نمایش می‌دهد
        System.out.println(new String(data));
    }

}

اگر این برنامه را با دو آرگومان [admin admin] اجرا کنیم:

  

نتیجه زیر را به دست می‌آوریم:

YWRtaW46YWRtaW4=

اکنون که می‌دانیم چگونه هدر احراز هویت HTTP را تولید کنیم، سرویس وب امن را راه‌اندازی می‌کنیم، سپس با استفاده از کلاینت کروم [Advanced Rest Client]، فهرستی از تمام محصولات را درخواست می‌کنیم:

  • در [1]، ما URL را برای دسته‌بندی‌ها درخواست می‌کنیم؛
  • در [2]، با استفاده از متد GET؛
  • در [3]، هدر احراز هویت HTTP را ارائه می‌دهیم. کد [YWRtaW46YWRtaW4=] رمزگذاری Base64 رشته [admin:admin] است؛
  • در [4]، ما فرمان HTTP را ارسال می‌کنیم؛

پاسخ سرور به شرح زیر است:

  • در [1]، هدر احراز هویت HTTP;
  • در [2]، سرور پاسخ jSON را بازمی‌گرداند؛

فهرست دسته‌بندی‌ها در واقع بازگردانده می‌شود:

 

حال بیایید یک درخواست HTTP با هدر احراز هویت نادرست را امتحان کنیم. پاسخ به شرح زیر است:

  • در [1]: هدر احراز هویت HTTP;

پاسخ زیر را دریافت می‌کنیم:

  • به [2]: پاسخ سرویس وب؛

حالا بیایید کاربر «user» / «user» را امتحان کنیم. این کاربر وجود دارد اما به سرویس وب دسترسی ندارد. اگر برنامه رمزگذاری Base64 را با هر دو آرگومان اجرا کنیم [user user]:

  

نتیجه زیر را دریافت می‌کنیم:

dXNlcjp1c2Vy
  • در [1]: هدر احراز هویت HTTP نادرست است؛
  • به [2]: پاسخ سرویس وب. این پاسخ با پاسخ قبلی که [401 Unauthorized] بود، متفاوت است. این بار، کاربر با موفقیت احراز هویت شده است اما برای دسترسی به URL مجوز کافی ندارد؛

سرویس وب امن ما اکنون عملیاتی است.

16.4.10. پاسخ احراز هویت: URL

  

ما قصد داریم یک URL بسازیم که به ما امکان می‌دهد تشخیص دهیم آیا کاربر مجاز به دسترسی به سرویس وب است یا خیر. برای این کار، ما کنترلر جدید زیر را ایجاد می‌کنیم: MVC [AuthenticateController]:


package spring.security.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import spring.webjson.models.Response;

@Controller
public class AuthenticateController {

    //وابستگی‌های Spring
    @Autowired
    private ApplicationContext context;

    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {
        //پاسخ: jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        return mapperResponse.writeValueAsString(new Response<Void>(0, null, null));
    }

}
  • خط ۱۵: کلاس [AuthenticateController] یک کنترل‌کننده Spring است. به همین دلیل، آن URL را در معرض دید قرار می‌دهد؛
  • خط ۲۲: URL و [/authenticate] را در معرض دید قرار می‌دهد؛
  • خط ۲۳: نتیجه متد مستقیماً به کلاینت ارسال می‌شود؛
  • خطوط ۲۶–۲۷: این متد به سادگی یک شیء خالی [Response] را بازمی‌گرداند، اما با مقدار ۰ در [status] که نشان می‌دهد هیچ خطایی رخ نداده است؛

این URL برای چیست؟ وقتی صرفاً می‌خواهیم یک کاربر را احراز هویت کنیم، آن را درخواست می‌کنیم. دیده‌ایم که اگر لایه امنیتی این کاربر را نپذیرد، یک استثنا پرتاب می‌کند. در اینجا یک مثال؛

با کاربر [admin:admin]:

ما یک پاسخ خالی دریافت می‌کنیم اما هیچ خطایی رخ نمی‌دهد.

با کاربر [user:user]:

یک استثنا پرتاب شد.

16.4.11. نتیجه‌گیری

کلاس‌های لازم برای Spring Security بدون هیچ تغییری در پروژه اصلی وب/JSON اضافه شدند. این سناریوی بسیار مطلوب ناشی از این واقعیت است که سه جدول افزوده‌شده به پایگاه داده مستقل از جدول‌های موجود هستند. حتی می‌توانستیم آن‌ها را در یک پایگاه داده جداگانه قرار دهیم. در موارد دیگر، جدول‌های افزوده‌شده ممکن است با جدول‌های موجود رابطه داشته باشند. در چنین مواردی، باید اشیاء JPA اصلاح شوند، که این امر عموماً بر تمام لایه‌های پروژه تأثیر می‌گذارد.

16.5. یک کلاینت برای سرویس وب امن / jSON برنامه‌نویسی شده است

ما قبلاً یک کلاینت برای سرویس وب ناامن / jSON نوشته‌ایم:

اکنون یک کلاینت برنامه‌ریزی‌شده برای سرویس وب امن ایجاد خواهیم کرد:

ما در حال کلون کردن پروژه موجود [intro-webjson-client] به یک پروژه جدید [intro-spring-security-client-01] هستیم:

  

16.5.1. کلاس [AbstractDao]

کلاس [AbstractDao] ارتباط بین HTTP و وب سرور امن / jSON را مدیریت می‌کند. همان‌طور که دیدیم، در این ارتباط HTTP، کلاینت اکنون باید یک هدر احراز هویت ارسال کند، برای مثال:

Authorization:Basic YWRtaW46YWRtaW4=

این کار به شرح زیر انجام می‌شود:


package spring.security.client.dao;

import java.net.URI;
...

public abstract class AbstractDao {

    // داده‌ها
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // درخواست عمومی
    protected String getResponse(User user, String url, String jsonPost) {

// URL: URL – تماس

  • خط ۱۵: متد عمومی [getResponse]، که ارتباط HTTP با سرویس وب امن را مدیریت می‌کند، اکنون به عنوان اولین پارامتر خود کاربر درخواست‌کننده یک URL را می‌پذیرد. کلاس [User] به شرح زیر است:

این کلاس به شرح زیر است:

  

package spring.security.client.entities;

public class User {

    // ویژگی‌ها
    private String login;
    private String password;

    // سازنده
    public User() {
    }

    public User(String login, String password) {
        this.login = login;
        this.password = password;
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...
}

بنابراین متد [getResponse] به شرح زیر است:


package spring.security.client.dao;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.web.client.RestTemplate;

import spring.security.client.entities.User;

public abstract class AbstractDao {

    // داده‌ها
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    private String getBase64(User user) {
        // کاربر و رمز عبور آن در Base64 رمزگذاری شده‌اند – نیازمند جاوا ۸
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }

    // درخواست عمومی
    protected String getResponse(User user, String url, String jsonPost) {

        // URL: URL – لطفاً تماس بگیرید
        //jsonPost: مقدار jSON برای ارسال
        try {
            // اجرای درخواست
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }

            // اجرای درخواست
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }

}

  • خطوط ۴۲–۴۴، ۴۹–۵۱: اگر کاربر [user] غیرخالی باشد، آنگاه هدر احراز هویت اضافه می‌شود. رمزنگاری نام کاربری و رمز عبور کاربر (Base64) توسط متد [getBase64] در خطوط 25–29 انجام می‌شود. توجه داشته باشید که این روش از کلاسی به نام [Base64] متعلق به JDK نسخه ۱.۸ استفاده می‌کند.
  • به جز خطوط قبلی، کد بدون تغییر باقی می‌ماند؛

16.5.2. رابط [IDao]

تمام متدهای رابط [IDao] پارامتر اضافی [User user] را دریافت می‌کنند:

  

package spring.security.client.dao;

import java.util.List;

import spring.security.client.entities.Categorie;
import spring.security.client.entities.Produit;
import spring.security.client.entities.User;

public interface IDaoClient {

    // احراز هویت
    public void authenticate(User user);

    // وارد کردن فهرست محصولات
    public List<Produit> addProduits(User user, List<Produit> produits);

    // حذف تمام محصولات
    public void deleteAllProduits(User user);

    // به‌روزرسانی فهرست محصولات
    public List<Produit> updateProduits(User user, List<Produit> produits);

    //بازیابی تمام محصولات
    public List<Produit> getAllProduits(User user);

    // افزودن فهرست دسته‌بندی‌ها
    public List<Categorie> addCategories(User user, List<Categorie> categories);

    // حذف تمام دسته‌بندی‌ها
    public void deleteAllCategories(User user);

    //به‌روزرسانی فهرست دسته‌بندی‌ها
    public List<Categorie> updateCategories(User user, List<Categorie> categories);

    // بازیابی همه دسته‌بندی‌ها
    public List<Categorie> getAllCategories(User user);

    // یک محصول خاص
    public Produit getProduitByIdWithCategorie(User user, Long idProduit);

    public Produit getProduitByIdWithoutCategorie(User user, Long idProduit);

    public Produit getProduitByNameWithCategorie(User user, String nom);

    public Produit getProduitByNameWithoutCategorie(User user, String nom);

    //یک دسته‌بندی خاص
    public Categorie getCategorieByIdWithProduits(User user, Long idCategorie);

    public Categorie getCategorieByIdWithoutProduits(User user, Long idCategorie);

    public Categorie getCategorieByNameWithProduits(User user, String nom);

    public Categorie getCategorieByNameWithoutProduits(User user, String nom);

}
  • خط ۱۲: ما متد [authenticate(User user)] را برای احراز هویت کاربر اضافه کرده‌ایم. این متد در صورتی که کاربر اجازه دسترسی به سرویس وب URL [/authenticate] را نداشته باشد، یک استثنا پرتاب می‌کند؛

16.5.3. کلاس [Dao]

تمام متدهای کلاس [Dao] یک پارامتر اضافی به نام [User user] دریافت می‌کنند که آن را به متد عمومی [getResponse] در کلاس [AbstractDao] ارسال می‌کنند. در اینجا دو مثال آورده شده است:


//احراز هویت
    @Override
    public void authenticate(User user) {
        getResponse(user, "/authenticate", null);
    }

    @Override
    public List<Produit> addProduits(User user, List<Produit> produits) {
        // ----------- افزودن محصولات (بدون دسته‌بندی آنها)
        try {
            // نقشه‌سازان jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // درخواست
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // خطا؟
            if (response.getStatus() != 0) {
                // ۱ استثنا پرتاب می‌شود
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                //بدنه پاسخ سرور را بازمی‌گرداند
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (IOException | RuntimeException e2) {
            throw new DaoException(100, e2);
        }
    }

16.5.4. تست‌های واحد برای کلاس [Dao]

کلاس [Test01] برای تست‌های واحد کلاس [Dao] به شرح زیر اصلاح شده است:

  

package client.tests.junit;

...

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // کانتکست Spring
    @Autowired
    private ApplicationContext context;
    // [DAO] لایه
    @Autowired
    private IDaoClient dao;

    // کاربران
    static private User admin;
    static private User user;
    static private User unknown;

    @BeforeClass
    public static void init() {
        admin = new User("admin", "admin");
        user = new User("user", "user");
        unknown = new User("x", "y");
    }

    @Before
    public void cleanAndFill() {
        //پایگاه داده قبل از هر آزمون پاک می‌شود
        log("Vidage de la base de données", 1);
        //جدول [CATEGORIES] تخلیه می‌شود – در نتیجه، جدول [PRODUITS] نیز تخلیه خواهد شد
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        //جداول پر می‌شوند
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // افزودن دسته‌بندی – در نتیجه، محصولات نیز درج خواهند شد
        dao.addCategories(admin, categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // فهرست دسته‌بندی‌ها
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories(admin);
        affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
        // فهرست محصولات
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits(admin);
        affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
        // چند بررسی
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }
...
    @Test()
    public void checkUserUser() {
        ServiceException se = null;
        try {
            dao.authenticate(user);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getMessages().get(0));
    }

    @Test()
    public void checkUserUnknown() {
        ServiceException se = null;
        try {
            dao.authenticate(unknown);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("401 Unauthorized", se.getMessages().get(0));
    }

    @Test()
    public void checkUserAdmin() {
        ServiceException se = null;
        try {
            dao.authenticate(admin);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNull(se);
    }
...
}
  • در حین инициализация کلاس تست، در خطوط 21–26، سه کاربر ایجاد می‌شوند:
    • کاربر [admin] به URL سرویس وب دسترسی دارد، خطوط تست 96–104؛
    • کاربر [user] وجود دارد اما مجاز به استفاده از URL سرویس وب نیست، خطوط تست 71–81؛
    • کاربر [unknown] وجود ندارد، خطوط تست 83–93؛
  • روش‌های آزمایشی همانند موارد پیشین برای سرویس وب ناامن هستند، با این تفاوت که متدهای رابط [IDaoClient] با کاربر [admin]، که مجاز به استفاده از URL است، به عنوان اولین پارامتر فراخوانی می‌شوند؛

آزمون با موفقیت انجام می‌شود، اما واضح است که نسبت به سرویس وب ناامن کندتر است. ایمن‌سازی یک برنامه به‌طور قابل‌توجهی زمان پاسخ آن را افزایش می‌دهد. یک عامل مهم در مورد عملکرد سرویس وب امن وجود دارد: در کلاس [AppConfig] که آن را پیکربندی می‌کند، نوشته‌ایم:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //CSRF
        http.csrf().disable();
        // برنامه امن؟
        if (activateSecurity) {
            // رمز عبور از طریق هدر Authorization: Basic xxxx ارسال می‌شود
            http.httpBasic();
            //روش HTTP OPTIONS باید برای همه مجاز باشد
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            //تنها نقش ADMIN می‌تواند از برنامه استفاده کند
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // تمام URL
                    .hasRole("ADMIN");
            // هیچ جلسه‌ای وجود ندارد
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

خط ۱۷ هزینه‌ای در پی دارد. این خط کاربر را مجبور می‌کند تا در هر دسترسی احراز هویت کند. اگر آن را غیرفعال کنیم، مدت زمان تست قبلی JUnit از ۱۰.۵۷ ثانیه به ۴.۲۱ ثانیه کاهش می‌یابد، این به این دلیل است که کاربر [admin] فقط برای اولین تست احراز هویت می‌شود و نه برای تست‌های بعدی (با اینکه هدر احراز هویت HTTP توسط کلاینت ارسال می‌شود، سرور رمز عبور کاربر را دوباره تأیید نمی‌کند). با یک سرویس وب ناامن، مدت زمان تست JUnit به ۲٫۳۳ ثانیه کاهش می‌یابد.