Skip to content

20. تأمين خدمة الويب للوصول إلى قاعدة البيانات [dbproduitscategories]

20.1. إعداد بيئة التطوير

سنقوم بتنفيذ أمان خدمة الويب باستخدام المشاريع التالية:

  
  • يمكن العثور على مشاريع [spring-security-*] في المجلد [<examples>\spring-database-generic\spring-security
  • سيتم تنفيذ الأمان لنظام إدارة قواعد البيانات MySQL باستخدام طبقة [DAO / JDBC] تليها طبقة [DAO / JPA / Hibernate
  • اضغط على Alt-F5 ثم أعد إنشاء جميع مشاريع Maven؛

نحتاج إلى إنشاء مستخدمين في قاعدة البيانات [dbproduitscategories]. للقيام بذلك، استخدم تكوين التشغيل [spring-security-create-users-hibernate-eclipselink]:

يؤدي تشغيل هذا التكوين إلى ملء الجداول [USERS، ROLES، USERS_ROLES] في قاعدة البيانات [dbproduitscategories]:

 

بيانات الاعتماد التي تم إنشاؤها [اسم المستخدم/كلمة المرور] هي كما يلي: [admin/admin]، [user/user]، [guest/guest]. يتم تشفير كلمات المرور في قاعدة البيانات.

Image

بمجرد الانتهاء من ذلك، قم بتشغيل التكوين الاختباري المسمى [spring-security-server-jpa-generic-hibernate-eclipselink]، والذي يقوم بتشغيل خدمة الويب الآمنة (يجب أن يكون MySQL قيد التشغيل):

ثم قم بتشغيل تكوين الاختبار المسمى [spring-security-client-generic-JUnitTestDao]، الذي يختبر خدمة الويب الآمنة:

يجب أن ينجح الاختبار.

20.2. مشروع Eclipse [spring-security-server-jdbc-generic]

يتم تنفيذ خدمة الويب الآمنة بواسطة مشروع [spring-security-server-jdbc-generic]:

أعلاه:

  • طبقة [DAO1] هي طبقة [DAO] التي تدير الجداول [PRODUCTS] و [CATEGORIES] في قاعدة البيانات [dbproduitscategories]. وقد تمت كتابتها بالفعل؛
  • الطبقة [DAO2] هي طبقة [DAO] التي تدير جداول [USERS] و[ROLES] و[USERS_ROLES] في قاعدة البيانات [dbproduitscategories]. لم يتم كتابتها بعد؛

  

20.2.1. تكوين Maven

مشروع [spring-security-server-jdbc-generic] هو مشروع 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>dvp.spring.database</groupId>
    <artifactId>spring-security-server-jdbc-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>spring-security-server-jdbc-generic</name>
    <description>démo spring security</description>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- web server / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jdbc-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • الأسطر 29–33: نعيد استخدام الكود الموجود مع أرشيف خدمة الويب / json / jdbc الذي درسناه؛
  • الأسطر 24–27: التبعية التي تجلب فئات Spring Security؛

في النهاية، يحتوي المشروع على التبعيات التالية للمشاريع الأخرى التي تم تحميلها في Eclipse:

  

20.2.2. طبقة [DAO2]

أعلاه:

  • طبقة [DAO1] هي طبقة [DAO] التي تدير الجداول [PRODUCTS] و [CATEGORIES] في قاعدة البيانات [dbproduitscategories]. وقد تمت كتابتها بالفعل؛
  • الطبقة [DAO2] هي طبقة [DAO] التي تدير جداول [USERS] و[ROLES] و[USERS_ROLES] في قاعدة البيانات [dbproduitscategories]. هذه هي الطبقة التي سنقوم بكتابتها الآن؛
  

يتطلب Spring Security إنشاء فئة تنفذ واجهة [UsersDetail] التالية:

 

يتم تنفيذ هذه الواجهة هنا بواسطة فئة [AppUserDetails]:


package spring.security.dao;
 
import generic.jdbc.config.ConfigJdbc;
 
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
 
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
import spring.jdbc.entities.Role;
import spring.jdbc.entities.User;
import spring.jdbc.infrastructure.DaoException;
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // JdbcTemplate
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    // properties
    private User user;
    private String simpleClassName = getClass().getSimpleName();
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.user = user;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
...
 
    // méthodes privées----------------
    private List<Role> getRoles(Long id) {
        try {
            // search for the user by id
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ROLES_BYUSERID, Collections.singletonMap("id", id),    new ShortRoleMapper());
        } catch (Exception e) {
            //e.printStackTrace();
            throw new DaoException(167, e, simpleClassName);
        }
    }
 
}
 
// --------------------- mappers
class ShortRoleMapper implements RowMapper<Role> {
 
    @Override
    public Role mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Role(rs.getLong("r_ID"), rs.getLong("r_VERSIONING"), rs.getString("r_NAME"));
    }
}
  • السطر 22: تنفذ فئة [AppUserDetails] واجهة [UserDetails
  • السطران 29-30: تغلف الفئة مستخدمًا (السطر 19) والمستودع الذي يوفر تفاصيل عن هذا المستخدم (السطر 20)؛
  • السطر 27: سيتم الوصول إلى قاعدة البيانات عبر JDBC باستخدام كائن [NamedParameterJdbcTemplate namedParameterJdbcTemplate] المحدد في مشروع [spring-jdbc-generic-04]. لاحظ أن هذا الكائن لا يتم حقنه بواسطة Spring كما هو معتاد. يتم توفيره للمُنشئ في الأسطر 36–39. لماذا؟ لأن فئة [AppUserDetails] ليست مكونًا من مكونات Spring (تفتقر إلى تعليق @Component) وبالتالي لا يمكن حقنها؛
  • الأسطر 36-39: المنشئ الذي ينشئ مثيلًا للفئة باستخدام مستخدم ومستودعه؛
  • الأسطر 42–49: تنفيذ طريقة [getAuthorities] لواجهة [UserDetails]. يجب أن تنشئ مجموعة من العناصر من النوع [GrantedAuthority] أو نوع مشتق. هنا، نستخدم النوع المشتق [SimpleGrantedAuthority] (السطر 46)، الذي يغلف اسم أحد أدوار المستخدم من السطر 29؛
  • الأسطر 45-47: نكرر قائمة أدوار المستخدم من السطر 29 لإنشاء قائمة من العناصر من النوع [SimpleGrantedAuthority
  • السطر 45: للحصول على أدوار المستخدم، نستخدم الطريقة الخاصة [getRoles] من السطر 53؛
  • السطر 56: ينفذ عبارة SQL التالية [ConfigJdbc.SELECT_ROLES_BYUSERID] (المحددة في [ConfigJdbc]):

public static final String SELECT_ROLES_BYUSERID = "SELECT DISTINCT r.ID as r_ID, r.VERSIONING as r_VERSIONING, r.NAME as r_NAME FROM ROLES r, USERS u, USERS_ROLES ur WHERE u.ID=:id AND ur.USER_ID=u.ID AND ur.ROLE_ID=r.ID";

يقوم استعلام SQL هذا بإجراء ربط بين الجداول الثلاثة [USERS، ROLES، USERS_ROLES] لاسترداد أدوار مستخدم محدد بواسطة مفتاحه الأساسي. ويتم تحديد معلماته بواسطة المفتاح الأساسي [:id] للمستخدم الذي يتم الاستعلام عن أدواره.

  • السطر 56: يتم تحويل كل صف من نتائج [SELECT] إلى كيان [Role] بواسطة فئة [ShortRowMapper] في الأسطر 66–72؛

لنعد إلى كود فئة [AppUserDetails]:


package spring.security.dao;
 
...
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // JdbcTemplate
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    // properties
    private User user;
    private String simpleClassName = getClass().getSimpleName();
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.user = user;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : 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;
    }
 
    // getters and setters
    ...
}
  • الأسطر 35–37: تنفيذ طريقة [getPassword] لواجهة [UserDetails]. نُرجع كلمة مرور المستخدم من السطر 12؛
  • الأسطر 39–42: تنفيذ طريقة [getUserName] لواجهة [UserDetails]. نُرجع اسم تسجيل دخول المستخدم من السطر 12؛
  • الأسطر 44–47: لا تنتهي صلاحية حساب المستخدم أبدًا؛
  • الأسطر 49–52: لا يتم قفل حساب المستخدم أبدًا؛
  • الأسطر 54–57: بيانات اعتماد المستخدم لا تنتهي صلاحيتها أبدًا؛
  • الأسطر 59–62: حساب المستخدم نشط دائمًا؛

يتطلب Spring Security أيضًا وجود فئة تنفذ واجهة [AppUserDetailsService]:

 

يتم تنفيذ هذه الواجهة بواسطة فئة [AppUserDetailsService] التالية:


package spring.security.dao;
 
import generic.jdbc.config.ConfigJdbc;
 
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
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.jdbc.entities.User;
import spring.jdbc.infrastructure.DaoException;
 
@Service
public class AppUserDetailsService implements UserDetailsService {
 
    // injections
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
 
    // local
    private String simpleClassName = getClass().getSimpleName();
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        List<User> users;
        try {
            // search for user via login
            users = namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_USER_BYLOGIN,
                    Collections.singletonMap("login", login), new ShortUserMapper());
        } catch (Exception e) {
            throw new DaoException(145, e, simpleClassName);
        }
        // found?
        if (users.size() == 0) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(users.get(0), namedParameterJdbcTemplate);
    }
}
 
// --------------------- mappers
class ShortUserMapper implements RowMapper<User> {
 
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new User(rs.getLong("u_ID"), rs.getLong("u_VERSIONING"), rs.getString("u_NAME"), rs.getString("u_LOGIN"),
                rs.getString("u_PASSWORD"));
    }
}
  • السطر 21: ستكون الفئة مكونًا من مكونات Spring؛
  • السطران 25-26: سيتم الوصول إلى قاعدة البيانات عبر JDBC باستخدام كائن [NamedParameterJdbcTemplate] المحدد في حبوب مشروع [spring-jdbc-generic-04
  • الأسطر 31-49: تنفيذ طريقة [loadUserByUsername] لواجهة [UserDetailsService] (السطر 22). المعلمة هي اسم تسجيل دخول المستخدم؛
  • السطران 36-37: يتم البحث عن المستخدم من خلال اسم تسجيل الدخول الخاص به. عبارة SQL [ConfigJdbc.SELECT_USER_BYLOGIN] هي كما يلي:

public static final String SELECT_USER_BYLOGIN = "SELECT u.ID as u_ID, u.VERSIONING as u_VERSIONING, u.NAME as u_NAME,u.LOGIN as u_LOGIN,u.PASSWORD as u_PASSWORD FROM USERS u WHERE u.LOGIN= :login";

يتم تحويل كل صف يتم إرجاعه بواسطة عبارة SELECT إلى كيان [User] بواسطة فئة [ShortUserMapper] في الأسطر 52–58.

  • الأسطر 42–44: إذا لم يتم العثور عليه، يتم إصدار استثناء؛
  • السطر 46: يتم إنشاء كائن [AppUserDetails] وإرجاعه. وهو بالفعل من النوع [UserDetails] (السطر 32). يتم تمرير معلومتين إلى منشئه:
    • المستخدم الذي تم العثور عليه؛
    • كائن [namedParameterJdbcTemplate] الذي سيسمح لفئة [AppUserDetails] بالاستعلام عن قاعدة البيانات؛

20.2.3. طبقة [web]

يعتمد مشروع [spring-security-server-jdbc-generic] على مشروع [spring-webjson-server-jdbc-generic]:

  

يُنفذ هذا المشروع الطبقة [الويب]. ولا يحتاج إلى تعديل.

20.2.4. تكوين أمان المشروع

يتم تكوين المشروع بواسطة فئة [AppConfig] التالية:

1
  

لقد تعرفنا بالفعل على فئة تكوين Spring Security (انظر القسم 19.2.4):


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");
    }
}

سنتبع نفس الإجراء:

  • السطر 11: تعريف فئة تمتد من فئة [WebSecurityConfigurerAdapter
  • السطر 13: تعريف طريقة [configure(HttpSecurity http)] التي تحدد حقوق الوصول إلى عناوين URL المختلفة لخدمة الويب؛
  • السطر 19: تعريف طريقة [configure(AuthenticationManagerBuilder auth)] التي تحدد المستخدمين وأدوارهم؛

ستكون فئة [AppConfig] كما يلي:


package spring.security.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
 
import spring.security.dao.AppUserDetailsService;
import spring.webjson.server.config.WebConfig;
 
@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.dao", "spring.security.service" })
@Import({ spring.webjson.server.config.AppConfig.class, WebConfig.class })
public class AppConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;
 
    // security
    private boolean activateSecurity = true;
 
    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // authentication is performed by bean [appUserDetailsService]
        // the password is encrypted using the BCrypt hash algorithm
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (activateSecurity) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // session or not?
            //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • السطر 17: هذه الفئة هي فئة تكوين Spring؛
  • السطر 18: تمكين مكونات Spring Security؛
  • السطر 26: نسترد مكونات Spring من طبقة [DAO2] وتلك الموجودة في حزمة [spring.security.service]، والتي سنناقشها لاحقًا؛
  • السطر 23: يستورد الفاصوليا من مشروع [spring-webjson-server-jdbc-generic]، الذي ينفذ طبقة [web]. ومن بين هذه الفاصوليا أيضًا تلك الموجودة في طبقة [DAO1
  • السطران 22-23: يتم حقن فئة [AppUserDetails]، التي توفر الوصول إلى مستخدمي التطبيق؛
  • السطر 26: قيمة منطقية تؤمن (true) أو لا تؤمن (false) تطبيق الويب؛
  • الأسطر 28–33: تحدد طريقة [configure(HttpSecurity http)] المستخدمين وأدوارهم. وهي تأخذ [AuthenticationManagerBuilder] كمعلمة. يتم إثراء هذه المعلمة بمعلومتين (السطر 32):
    • إشارة إلى [appUserDetailsService] من السطر 23، والتي توفر الوصول إلى المستخدمين المسجلين. لاحظ هنا أن حقيقة تخزينهم في قاعدة بيانات لم يتم ذكرها صراحةً. لذلك، يمكن أن يكونوا في ذاكرة التخزين المؤقت، أو يتم توفيرهم بواسطة خدمة ويب، إلخ.
    • نوع التشفير المستخدم لكلمة المرور. استخدمنا خوارزمية BCrypt؛
  • الأسطر 35-53: تحدد طريقة [configure(HttpSecurity http)] حقوق الوصول إلى عناوين URL لخدمة الويب؛
  • السطر 38: رأينا في المشروع التمهيدي أن Spring Security تدير افتراضيًا رمز CSRF (تزوير الطلبات عبر المواقع) الذي يجب على المستخدم الراغب في المصادقة إرساله مرة أخرى إلى الخادم. هنا، يتم تعطيل هذه الآلية. هذا، جنبًا إلى جنب مع القيمة المنطقية (isSecured=false)، يسمح باستخدام تطبيق الويب بدون أمان؛
  • السطر 42: نقوم بتمكين المصادقة عبر رؤوس HTTP. يجب على العميل إرسال رأس HTTP التالي:
Authorization:Basic code

حيث code هو ترميز Base64 لسلسلة login:password. على سبيل المثال، ترميز Base64 لسلسلة admin:admin هو YWRtaW46YWRtaW4=. لذلك، سيقوم المستخدم الذي يستخدم اسم المستخدم [admin] وكلمة المرور [admin] بإرسال رأس HTTP التالي للمصادقة:

Authorization:Basic YWRtaW46YWRtaW4=
  • الأسطر 47–49: تشير إلى أن جميع عناوين URL لخدمة الويب متاحة للمستخدمين الذين لديهم دور [ROLE_ADMIN]. وهذا يعني أن المستخدم الذي لا يمتلك هذا الدور لا يمكنه الوصول إلى خدمة الويب؛
  • السطر 51: في وضع [session]، لا يحتاج المستخدم الذي قام بالمصادقة مرة واحدة إلى القيام بذلك في عمليات الوصول اللاحقة. هذا هو الإعداد الافتراضي لـ Spring Security. السطر 51 يعطل هذا الوضع. إذا تم تمكينه، يجب على المستخدم المصادقة عند كل عملية وصول. بدون جلسة عمل، تكون خدمة الويب الآمنة أقل استجابة مقارنة بوجود جلسة عمل، لذلك تم تعليق السطر 51؛

20.2.5. اختبار خدمة الويب الآمنة

سنقوم باختبار خدمة الويب باستخدام عميل Chrome [Advanced Rest Client]. سنحتاج إلى تحديد رأس مصادقة HTTP:

Authorization:Basic code

حيث [code] هي السلسلة المشفرة بـ Base64 [login:password]. لإنشاء هذا الرمز، يمكنك استخدام البرنامج التالي من مشروع [spring-security-create-users]:

  

package spring.security.helpers;
 
import org.springframework.security.crypto.codec.Base64;
 
public class Base64Encoder {
 
    public static void main(String[] args) {
        // we expect two arguments: login password
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // we retrieve the two arguments
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // encode the string
        byte[] data = Base64.encode(chaîne.getBytes());
        // displays its Base64 encoding
        System.out.println(new String(data));
    }
 
}

إذا قمنا بتشغيل هذا البرنامج مع الحجتين [admin admin]:

  

نحصل على النتيجة التالية:

YWRtaW46YWRtaW4=

نحن الآن جاهزون للاختبار:

  • يجب أن يكون نظام إدارة قواعد البيانات MySQL قيد التشغيل؛
  • نقوم بتعبئة الجداول [PRODUCTS] و [CATEGORIES] باستخدام تكوين التنفيذ المسمى [spring-jdbc-generic-04-fillDataBase]:
 
  • إذا لم يكن ذلك قد تم بالفعل، نقوم بملء الجداول [USERS، ROLES، USERS_ROLES] باستخدام تكوين التنفيذ المسمى [spring-security-create-users-hibernate-eclipselink]:
 
  • نقوم بتشغيل خدمة الويب الآمنة باستخدام تكوين وقت التشغيل المسمى [spring-security-server-jdbc-generic]:
 

ثم، باستخدام عميل Chrome [Advanced Rest Client]، نطلب النسخة الطويلة لجميع الفئات:

  • في [1]، نطلب عنوان URL الخاص بالأوصاف الطويلة للفئات؛
  • في [2]، باستخدام طريقة GET؛
  • في [3]، نقدم رأس HTTP للمصادقة. الرمز [YWRtaW46YWRtaW4=] هو ترميز Base64 للسلسلة [admin:admin
  • في [4]، نرسل طلب HTTP؛

استجابة الخادم هي كما يلي:

  • في [1]، رأس مصادقة HTTP؛
  • في [2]، يقوم الخادم بإرجاع استجابة JSON؛

نجحنا في الحصول على قائمة الفئات:

 

الآن دعونا نجرب طلب HTTP برأس مصادقة غير صحيح. تكون الاستجابة عندئذ كما يلي:

  • في [1]: رأس المصادقة HTTP؛

نتلقى الرد التالي:

  • في [2]: استجابة خدمة الويب؛

الآن، دعونا نجرب المستخدم / user. إنه موجود ولكنه لا يملك حق الوصول إلى خدمة الويب. إذا قمنا بتشغيل برنامج الترميز Base64 مع الحجتين [user user]:

  

نحصل على النتيجة التالية:

dXNlcjp1c2Vy
  • في [1]: رأس مصادقة HTTP غير صحيح؛
  • في [2]: استجابة خدمة الويب. وهي تختلف عن الاستجابة السابقة، التي كانت [401 غير مصرح به]. هذه المرة، تمت مصادقة المستخدم بشكل صحيح ولكنه لا يمتلك أذونات كافية للوصول إلى عنوان URL؛

أصبحت خدمة الويب الآمنة جاهزة للعمل الآن.

20.2.6. عنوان URL للمصادقة

  

سننشئ عنوان URL يسمح لنا بتحديد ما إذا كان المستخدم مخولًا للوصول إلى خدمة الويب. للقيام بذلك، نقوم بإنشاء وحدة تحكم MVC جديدة [AuthenticateController] كما يلي:


package spring.security.service;
 
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
import spring.webjson.server.service.Response;
 
@RestController
public class AuthenticateController {
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET)
    public Response<Void> authenticate() {
        return new Response<Void>(0, null, null);
    }
 
}
  • السطر 9: فئة [AuthenticateController] هي وحدة تحكم Spring. وبصفتها كذلك، فإنها تعرض عناوين URL. تشير العلامة [@RestController] إلى أن الطرق التي تتعامل مع عناوين URL هذه ترسل استجاباتها الخاصة إلى العميل؛
  • السطر 11: يعرض عنوان URL [/authenticate
  • الأسطر 12–14: تعرض الطريقة ببساطة كائن [Response] فارغًا ولكن مع [status] يساوي 0، مما يشير إلى عدم حدوث أي خطأ؛

ما الغرض من عنوان URL هذا؟ عندما نريد ببساطة مصادقة مستخدم، سنطلبه. وقد رأينا أنه إذا لم تقبل طبقة الأمان هذا المستخدم، فإنها ترمي استثناءً. وإليك مثال على ذلك؛

مع المستخدم [admin:admin]:

نحصل على استجابة فارغة ولكن دون حدوث استثناء.

مع المستخدم [user:user]:

حدثت استثناء.

20.2.7. الخلاصة

تمت إضافة الفئات اللازمة لـ Spring Security دون تعديل مشروع web/JSON الأصلي. وينبع هذا السيناريو المثالي من حقيقة أن الجداول الثلاثة التي أُضيفت إلى قاعدة البيانات مستقلة عن الجداول الموجودة. بل كان بإمكاننا وضعها في قاعدة بيانات منفصلة. وفي حالات أخرى، قد تكون للجداول المضافة علاقات مع الجداول الموجودة. وفي هذه الحالة، يجب مراجعة الكود الموجود في طبقة [DAO] الحالية.

20.3. عميل مبرمج لخدمة web/JSON الآمنة

لقد قمنا بالفعل بكتابة عميل لخدمة الويب / JSON غير الآمنة:

سنقوم الآن بإنشاء عميل مبرمج لخدمة الويب الآمنة:

  

20.3.1. طبقة [عميل HTTP]

 

تتولى فئة [Client] إدارة الاتصال عبر HTTP مع خادم الويب الآمن / JSON. وكما رأينا للتو، في هذا الاتصال عبر HTTP، يجب على العميل الآن إرسال رأس مصادقة، على سبيل المثال:

Authorization:Basic YWRtaW46YWRtaW4=

تصبح واجهة [IClient] كما يلي:


package spring.webjson.client.dao;
 
import org.springframework.http.HttpMethod;
 
import spring.webjson.client.entities.Credentials;
 
public interface IClient {
    public <T1, T2> T1 getResponse(Credentials credentials,String url, HttpMethod method, int errStatus, T2 body);
}
  • السطر 8: أصبح المعامل الأول لطريقة [getResponse] الآن كائن [Credentials] الذي يغلف بيانات اعتماد المستخدم:

package spring.security.client.entities;
 
public class Credentials {
 
    // properties
    private String login;
    private String password;
 
    // manufacturer
    public Credentials() {
    }
 
    public Credentials(String login, String password) {
        this.login = login;
        this.password = password;
    }
 
    // getters and setters
...
}

تتطور فئة [Client]، التي تنفذ واجهة [IClient]، على النحو التالي:


package spring.security.client.dao;
 
...
 
@Component
public class Client implements IClient {
 
    // injections
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // local
    private String simpleClassName = getClass().getSimpleName();
 
    private String getBase64(Credentials credentials) {
        // encodes user and password in base 64 - requires java 8
        String chaîne = String.format("%s:%s", credentials.getLogin(), credentials.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }
 
    // generic request
    @Override
    public <T1, T2> T1 getResponse(Credentials credentials, String url, HttpMethod method, int errStatus, T2 body) {
        // the server response
        ResponseEntity<Response<T1>> response;
        try {
            // prepare the query
            RequestEntity<?> request = null;
            if (method == HttpMethod.GET) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))    .accept(MediaType.APPLICATION_JSON);
                if (credentials != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(credentials));
                }
                request = headersBuilder.build();
            }
            if (method == HttpMethod.POST) {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url))).header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (credentials != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(credentials));
                }
                request = bodyBuilder.body(body);
            }
            // execute the query
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // encapsulate the exception
            throw new DaoException(errStatus, e, simpleClassName);
        }
...
    }
...
}
  • الأسطر 33–35، 40–42: إذا كانت [بيانات اعتماد] المستخدم غير فارغة، يتم إضافة رأس المصادقة. يتم التعامل مع ترميز Base64 لاسم المستخدم وكلمة المرور بواسطة طريقة [getBase64] في الأسطر 17–21. لاحظ أن هذه الطريقة تستخدم فئة [Base64] من JDK 1.8. يمكن لعميل HTTP الخاص بنا العمل مع خدمة ويب غير آمنة. ما عليك سوى تمرير [بيانات اعتماد] تساوي null؛
  • بصرف النظر عن الأسطر السابقة، يظل الكود دون تغيير؛

20.3.2. طبقة [DAO]

20.3.2.1. واجهة [IDao]

  

تستقبل جميع طرق واجهة [IDao] في مشروع [spring-webjson-client-generic] معلمة [Credentials] إضافية:


package spring.security.client.dao;
 
import java.util.List;
 
import spring.security.client.entities.AbstractCoreEntity;
import spring.security.client.entities.Credentials;
 
public interface IDao<T extends AbstractCoreEntity> extends IAuthenticate {
 
    // list of all T entities
    public List<T> getAllShortEntities(Credentials credentials);
 
    public List<T> getAllLongEntities(Credentials credentials);
 
    // special entities - short version
    public List<T> getShortEntitiesById(Credentials credentials, Iterable<Long> ids);
 
    public List<T> getShortEntitiesById(Credentials credentials, Long... ids);
 
    public List<T> getShortEntitiesByName(Credentials credentials, Iterable<String> names);
 
    public List<T> getShortEntitiesByName(Credentials credentials, String... names);
 
    // special entities - long version
    public List<T> getLongEntitiesById(Credentials credentials, Iterable<Long> ids);
 
    public List<T> getLongEntitiesById(Credentials credentials, Long... ids);
 
    public List<T> getLongEntitiesByName(Credentials credentials, Iterable<String> names);
 
    public List<T> getLongEntitiesByName(Credentials credentials, String... names);
 
    // update of several entities
    public List<T> saveEntities(Credentials credentials, Iterable<T> entities);
 
    public List<T> saveEntities(Credentials credentials, @SuppressWarnings("unchecked") T... entities);
 
    // delete all entities
    public void deleteAllEntities(Credentials credentials);
 
    // deletion of multiple entities
    public void deleteEntitiesById(Credentials credentials, Iterable<Long> ids);
 
    public void deleteEntitiesById(Credentials credentials, Long... ids);
 
    public void deleteEntitiesByName(Credentials credentials, Iterable<String> names);
 
    public void deleteEntitiesByName(Credentials credentials, String... names);
 
    public void deleteEntitiesByEntity(Credentials credentials, Iterable<T> entities);
 
    public void deleteEntitiesByEntity(Credentials credentials, @SuppressWarnings("unchecked") T... entities);
}
  • السطر 8: واجهة [IDao] تمتد واجهة [IAuthenticate] التالية:

package spring.security.client.dao;
 
import spring.security.client.entities.Credentials;
 
public interface IAuthenticate {
    // authentication
    public void authenticate(Credentials credentials);
}

تحتوي واجهة [IAuthenticate] على طريقة واحدة فقط، وهي [authenticate]. لا تُرجع هذه الطريقة أي قيمة (void) إذا تم قبول المستخدم [Credentials credentials] من قبل خدمة الويب الآمنة؛ وإلا، فإنها تُطلق استثناءً.

20.3.2.2. فئة [AbstractDao]

  

تذكر أن فئة [AbstractDao] هي الفئة الأم لفئات [DaoCategorie]، التي تدير عناوين URL للفئات، وفئة [DaoProduit]، التي تدير عناوين URL للمنتجات. تتلقى جميع طرق فئة [AbstractDao] في مشروع [spring-webjson-client-generic] معلمة إضافية [Credentials] تقوم بتمريرها إلى الفئة الفرعية. وإليك مثال على ذلك:


    @Override
    public List<T1> getShortEntitiesById(Credentials credentials, Iterable<Long> ids) {
        // validité de l'argument
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // résultat
        return getShortEntitiesById(credentials, Lists.newArrayList(ids));
}
  • تستقبل الطريقة [getShortEntitiesById] المعلمة [Credentials] (السطر 2)، ثم تقوم بتمريرها (السطر 9) إلى الطريقة [getShortEntitiesById] في الفئة الفرعية؛

تحتوي الفئة [AbstractDao] على الهيكل التالي:


package spring.security.client.dao;
 
import java.util.ArrayList;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
 
import spring.security.client.entities.AbstractCoreEntity;
import spring.security.client.entities.Credentials;
import spring.security.client.infrastructure.MyIllegalArgumentException;
 
import com.google.common.collect.Lists;
 
public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {
 
    @Autowired
    private IAuthenticate authenticate;
 
...
}
  • السطر 14: تنفذ الفئة واجهة [IDao] التي وصفناها؛
  • السطران 16-17: يتم إدخال مثيل لواجهة [IAuthenticate]. ويتم تنفيذ ذلك بواسطة الفئة [Authenticate] التالية:

package spring.security.client.dao;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
 
import spring.security.client.entities.Credentials;
 
@Component
public class Authenticate implements IAuthenticate{
    @Autowired
    protected IClient client;
 
    // verification [credentials,mdp]
    public void authenticate(Credentials credentials) {
        client.<Void, Void> getResponse(credentials, "/authenticate", HttpMethod.GET, 111, (Void) null);
    }
 
}
  • السطر 9: فئة [Authenticate] هي مكون من مكونات Spring؛
  • السطر 10: والتي تنفذ واجهة [IAuthenticate
  • السطران 11-12: حقن عميل HTTP الذي يتيح الاتصال بخدمة الويب الآمنة؛
  • السطور 15–17: تنفيذ طريقة [authenticate] للواجهة؛
  • السطر 16: يتم إرسال طلب HTTP GET إلى عنوان URL [/authenticate]. تم توضيح استخدام عنوان URL هذا في القسم 20.2.6. المبدأ هو أن الاستدعاء ينتج عنه استثناء إذا كانت [بيانات اعتماد] المستخدم غير معروفة أو تفتقر إلى أذونات كافية؛

تقوم فئة [AbstractDao] بتنفيذ طريقة [authenticate] الخاصة بواجهة [IDao] على النحو التالي:


    @Autowired
    private IAuthenticate authenticate;
 
    @Override
    public void authenticate(Credentials credentials) {
        authenticate.authenticate(credentials);
}
  • السطر 7: يتم تفويض المهمة إلى طريقة [authenticate] في فئة [Authenticate]. وبالتالي، سيتم إلقاء استثناء إذا لم تقبل خدمة الويب الآمنة بيانات اعتماد المستخدم [Credentials credentials

20.3.2.3. فئتا [DaoCategorie، DaoProduit]

  

فئات [DaoCategorie، DaoProduit] هي تلك الموجودة في مشروع [spring-webjson-server-generic] مع المعلمة الإضافية [Credentials credentials]. وإليك مثال على ذلك:


@Component
public class DaoCategorie extends AbstractDao<Categorie> {
 
    // injections
    @Autowired
    protected ApplicationContext context;
    @Autowired
    protected IClient client;
 
    @Override
    public List<Categorie> getAllShortEntities(Credentials credentials) {
        try {
            // filters jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // get all categories
            Object map = client.<List<Categorie>, Void> getResponse(credentials, "/getAllShortCategories", HttpMethod.GET,
                    202, null);
            // the List<Categorie> category list
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(221, e2, simpleClassName);
        }
    }
....

20.3.3. تكوين Spring

  

تقوم فئة [AppConfig] بتكوين بيئة Spring للمشروع. وهي مطابقة لما كانت عليه في مشروع [spring-webjson-client-generic]، باستثناء واحد:


@Configuration
@ComponentScan({ "spring.security.client.dao" })
public class AppConfig {
  • السطر 2: يجب تحديد حزمة طبقة [DAO] الجديدة؛

20.3.4. اختبارات طبقة [DAO]

  

20.3.4.1. اختبار [JUnitTestCredentials]

يستخدم اختبار [JUnitTestCredentials] طريقة [IDao.authenticate] للتحقق من صحة مستخدمين معينين:


package client.tests.junit;
 
import org.junit.Assert;
import org.junit.BeforeClass;
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.test.context.junit4.SpringJUnit4ClassRunner;
 
import spring.security.client.config.AppConfig;
import spring.security.client.dao.IAuthenticate;
import spring.security.client.entities.Credentials;
import spring.security.client.infrastructure.DaoException;
 
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestCredentials {

    // layer [DAO]
    @Autowired
    private IAuthenticate authenticate;
 
    // users
    static private Credentials admin;
    static private Credentials user;
    static private Credentials unknown;
 
    @BeforeClass
    public static void init() {
        admin = new Credentials("admin", "admin");
        user = new Credentials("user", "user");
        unknown = new Credentials("x", "y");
    }
 
    @Test()
    public void checkUserUser() {
        DaoException se = null;
        try {
            authenticate.authenticate(user);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserUser: " + e);
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getExceptions().get(0).getErrorMessage());
    }
 
    @Test()
    public void checkUserUnknown() {
        DaoException se = null;
        try {
            authenticate.authenticate(unknown);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserUnknown : " + e);
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("401 Unauthorized", se.getExceptions().get(0).getErrorMessage());
    }
 
    @Test()
    public void checkUserAdmin() {
        DaoException se = null;
        try {
            authenticate.authenticate(admin);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserAdmin : " + e);
        }
        Assert.assertNull(se);
    }
}
  • أثناء تهيئة فئة الاختبار، في الأسطر 29–34، يتم إنشاء ثلاثة مستخدمين:
    • المستخدم [admin] لديه حق الوصول إلى عناوين URL لخدمة الويب. يتم اختبار ذلك في الأسطر 63–72؛
    • المستخدم [user] موجود ولكنه غير مخول باستخدام عناوين URL لخدمة الويب. يتم اختبار ذلك في الأسطر 37–47؛
    • المستخدم [unknown] غير موجود. يتم اختبار ذلك في الأسطر 50–60؛

نقوم بتشغيل خدمة الويب الآمنة باستخدام تكوين وقت التشغيل المسمى [spring-security-server-jdbc-generic] [1]:

ثم نقوم بتشغيل اختبار JUnit [JUnitTestCredentials] باستخدام تكوين وقت التشغيل [spring-security-client-generic-JUnitTestCredentials] [2]. وفيما يلي نتائج وحدة التحكم التي تم الحصول عليها:

checkUserUser: [code=111, trace=[Client,getResponse,65], exceptions=[{"className":"org.springframework.web.client.HttpClientErrorException","errorMessage":"403 Forbidden"}]
checkUserUnknown : [code=111, trace=[Client,getResponse,65], exceptions=[{"className":"org.springframework.web.client.HttpClientErrorException","errorMessage":"401 Unauthorized"}]

وينجح الاختبار:

  

20.3.4.2. اختبار [JUnitTestDao]

اختبار [JUnitTestDao] مطابق لما كان عليه في مشروع [spring-webjson-client-generic] غير المؤمن، باستثناء أن طرق طبقة [DAO] التي يتم اختبارها الآن تحتوي جميعها على المستخدم [admin / admin] كمعلمة أولى:


@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {
 
    // spring context
    @Autowired
    private ApplicationContext context;
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
....
 
    // users
    static private Credentials admin;
 
    @BeforeClass
    public static void init() {
        admin = new Credentials("admin", "admin");
    }
 
    @Before
    public void clean() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // we empty table [CATEGORIES] and cascade table [PRODUITS]
        daoCategorie.deleteAllEntities(admin);
        // emptying dictionaries
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }
 
    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // fill the tables
        ...
        // adding the category - by cascading the products will also be
        // inserted - the result is returned at the same time
        return daoCategorie.saveEntities(admin, categories);
    }
 
    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities(admin);
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities(admin);
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // result
        return new Object[] { categories, produits };
    }
...

يتم تنفيذ جميع العمليات باستخدام المستخدم [admin / admin]، وهو المستخدم الوحيد الذي يمتلك حقوق الوصول إلى خدمة الويب الآمنة.

سنقوم بتشغيل الاختبار باستخدام تكوين التنفيذ المسمى [spring-security-client-generic-JUnitTestDao]:

 

نجح الاختبار، لكننا نلاحظ أنه أبطأ مقارنة بخدمة الويب غير المؤمنة. يؤدي تأمين التطبيق إلى زيادة كبيرة في أوقات الاستجابة. هناك عامل مهم واحد يؤثر على أداء خدمة الويب المؤمنة: في فئة [AppConfig] التي تقوم بتكوينها، كتبنا:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (activateSecurity) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

السطر 17 له تأثير. فهو يحدد ما إذا كان المستخدم مجبرًا على المصادقة عند كل وصول أم لا. إذا قمنا بتعليقه، فإن مدة اختبار JUnit تكون أقصر بكثير، لأن المستخدم [admin] يقوم بالمصادقة فقط في الاختبار الأول وليس في الاختبارات اللاحقة (حتى إذا تم إرسال رأس مصادقة HTTP من قبل العميل، فإن الخادم لا يعيد التحقق من كلمة مرور المستخدم).

20.4. مشروع Eclipse [spring-security-server-jpa-generic]

سيتم الآن تنفيذ خدمة الويب الآمنة بواسطة مشروع [spring-security-server-jpa-generic]، الذي يعتمد على مشروع [spring-jpa-generic] الذي يدير الوصول إلى قاعدة البيانات باستخدام Spring Data JPA:

أعلاه:

  • طبقة [DAO1] هي طبقة [DAO] التي تدير الجداول [PRODUCTS] و [CATEGORIES] في قاعدة البيانات [dbproduitscategories]. وقد تمت كتابتها بالفعل؛
  • الطبقة [DAO2] هي طبقة [DAO] التي تدير جداول [USERS] و[ROLES] و[USERS_ROLES] في قاعدة البيانات [dbproduitscategories]. لم يتم كتابتها بعد؛

تم إنشاء مشروع [spring-security-server-jpa-generic] في البداية عن طريق استنساخ المشروع الذي تمت دراسته سابقًا [spring-security-server-jdbc-generic]. في الواقع، تظل طبقات [web] و[security] دون تغيير للأسباب التالية:

  • طبقة [DAO1 / Repositories / JPA] (المكتوبة بالفعل) لها نفس واجهة طبقة [DAO1 / JDBC
  • ستكون لطبقة [DAO2 / Repositories / JPA] (التي سيتم كتابتها) نفس واجهة طبقة [DAO2 / JDBC

مشروع [spring-security-server-jpa-generic] هو كما يلي:

  
  • تنفذ الحزمة [spring.security.repositories] طبقة [repositories
  • حزمة [spring.security.dao] تنفذ طبقة [dao2

20.4.1. مشروع 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>dvp.spring.database</groupId>
    <artifactId>spring-security-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
 
    <name>spring-security-server-jpa-generic</name>
    <description>démo spring security</description>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- web server / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- plugins -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • الأسطر 24–27: التبعية الخاصة بطبقة [الأمان] للمشروع؛
  • الأسطر 29–33: التبعية لطبقة [web] الخاصة بالمشروع. يقوم مشروع [spring-webjson-server-jpa-generic] بتنفيذ طبقة [web] بالكامل. لا تحتاج هذه الطبقة إلى الكتابة أو التعديل؛

في النهاية، تكون التبعيات كما يلي:

  

20.4.2. تكوين الربيع

  

ملف التكوين [AppConfig] من المشروع السابق [spring-security-server-jdbc-generic] مناسب. ما عليك سوى إضافة تكوين إضافي إليه:


@Configuration
@EnableWebSecurity
@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao", "spring.security.service" })
@Import({ spring.webjson.server.config.AppConfig.class })
public class AppConfig extends WebSecurityConfigurerAdapter {
  • السطر 3: نعلن الحزمة التي تنفذ طبقة [repositories
  • السطر 4: الحزم التي تحتوي على حبوب Spring لها نفس الاسم في المشروع الجديد؛
  • السطر 5: في المشروع السابق، تم العثور على فئة [spring.webjson.server.config.AppConfig] في التبعية [spring-webjson-server-jdbc-generic]. هنا، سيتم العثور عليها في التبعية [spring-webjson-server-jpa-generic

20.4.3. طبقة JPA

توجد كيانات JPA التي تديرها طبقة [JPA] في مشروع [mysql-config-jpa-hibernate] [2]، وهو أحد التبعيات لمشروع [1]:

فئة [User] هي التعيين الخاص بجدول [USERS]:

Image

  • ID: المفتاح الأساسي؛
  • VERSION: عمود إصدار الصف؛
  • IDENTITY: معرف وصفية للمستخدم؛
  • LOGIN: اسم تسجيل دخول المستخدم؛
  • PASSWORD: كلمة مرور المستخدم؛

package generic.jpa.entities.dbproduitscategories;
 
import generic.jdbc.config.ConfigJdbc;
 
import java.util.List;
 
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
 
import com.fasterxml.jackson.annotation.JsonIgnore;
 
@Entity
@Table(name = ConfigJdbc.TAB_USERS)
public class User implements AbstractCoreEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
 
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;
 
    @Transient
    protected EntityType entityType=EntityType.POJO;
 
    // properties
    @Column(name = ConfigJdbc.TAB_USERS_NAME, length = 30, nullable = false)
    private String name;
    @Column(name = ConfigJdbc.TAB_USERS_LOGIN, length = 30, unique = true, nullable = false)
    private String login;
    @Column(name = ConfigJdbc.TAB_USERS_PASSWORD, length = 60, nullable = false)
    private String password;
 
    // the associated UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;
 
    // manufacturers
    public User() {
    }
 
    public User(Long id, Long version, String identity, String login, String password) {
        this.id = id;
        this.version = version;
        this.name = identity;
        this.login = login;
        this.password = password;
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [hashcode]
...
 
    // getters and setters
...
}
  • السطر 23: تنفذ الفئة واجهة [AbstractCoreEntity] المستخدمة بالفعل للكيانات الأخرى؛
  • السطران 34-35: نوع الكيان. لا يتم حفظ هذه الخاصية في قاعدة البيانات [@Transient
  • الأسطر 38–43: الخصائص الأساسية الثلاثة للمستخدم (الاسم، اسم المستخدم، كلمة المرور)؛
  • الأسطر 46–48: قائمة أدوار المستخدم. قد يكون للمستخدم أدوار متعددة. وبالمثل، سنرى أن الدور يمكن أن يرتبط بعدة مستخدمين. وبالتالي، بالمعنى الذي يقصده مصطلح JPA، توجد علاقة [ManyToMany] بين كيانات [User] و[Role]:
    • يمكن تعيين مستخدم لأدوار متعددة؛
    • يمكن ربط الدور بعدة مستخدمين؛

يتم تنفيذ هذه العلاقة [ManyToMany] في قاعدة البيانات من خلال جدول الربط [USERS_ROLES]. إذا كان للمستخدم U علاقة بدور R، يتم تخزين هذه العلاقة في جدول [USERS_ROLES] عن طريق تسجيل زوج المفتاح الأساسي للكيانات (U,R). من ناحية JPA، يمكن تقسيم العلاقة [ManyToMany] التي تربط كيانات [User] و [Role] إلى علاقتين [ManyToOne, OneToMany]:

  • (تابع)
    • علاقة [ManyToOne] من كيان [User] إلى كيان [UserRole
    • علاقة [OneToMany] من كيان [UserRole] إلى كيان [UserRole

وبالمثل، يمكن تقسيم العلاقة [ManyToMany] التي تربط كيانات [Role] و [User] إلى علاقتين [ManyToOne، OneToMany]:

  • (تابع)
    • علاقة [ManyToOne] من الكيان [Role] إلى الكيان [UserRole
    • علاقة [OneToMany] من كيان [UserRole] إلى كيان [User
  • السطر 48: يتم تمثيل حقيقة أن المستخدم لديه أدوار متعددة بعلاقة [OneToMany] مع كيان [UserRole

تمثل فئة [Role] الجدول [ROLES]:

Image

  • ID: المفتاح الأساسي؛
  • VERSION: عمود إصدار الصف؛
  • NAME: اسم الدور. بشكل افتراضي، يتوقع Spring Security أن تكون الأسماء بالصيغة ROLE_XX، على سبيل المثال ROLE_ADMIN أو ROLE_GUEST؛

package generic.jpa.entities.dbproduitscategories;
 
import generic.jdbc.config.ConfigJdbc;
 
import java.util.List;
 
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
 
import com.fasterxml.jackson.annotation.JsonIgnore;
 
@Entity
@Table(name = ConfigJdbc.TAB_ROLES)
public class Role implements AbstractCoreEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
    
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;
 
    @Transient
    protected EntityType entityType=EntityType.POJO;
 
    // properties
    @Column(name = ConfigJdbc.TAB_ROLES_NAME, length = 30, unique = true, nullable = false)
    private String name;
 
    // the associated UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "role", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;
 
    // manufacturers
    public Role() {
    }
 
    public Role(Long id, Long version, String name) {
        this.id = id;
        this.version = version;
        this.name = name;
    }
 
    // getters and setters
    public Role(String name) {
        this.name = name;
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [hashcode]
...
    // getters and setters
...
}
  • الأسطر 42–44: يتم تمثيل حقيقة أنه يمكن ربط عدة مستخدمين بدور ما من خلال علاقة [@OneToMany] بالكيان [UserRole

تمثل فئة [UserRole] الجدول [USERS_ROLES]:

Image

يمكن أن يكون للمستخدم أدوار متعددة، ويمكن أن يكون للدور مستخدمون متعددون. لدينا علاقة متعددة إلى متعددة تمثلها الجدول [USERS_ROLES].

  • ID: المفتاح الأساسي؛
  • VERSION: عمود إصدار الصف؛
  • USER_ID: معرف المستخدم؛
  • ROLE_ID: معرف الدور؛

package generic.jpa.entities.dbproduitscategories;
 
import generic.jdbc.config.ConfigJdbc;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
 
@Entity
@Table(name = ConfigJdbc.TAB_USERS_ROLES)
public class UserRole implements AbstractCoreEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
 
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;
 
    @Transient
    protected EntityType entityType=EntityType.POJO;
 
    // a UserRole refers to a User
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_USER_ID, nullable = false)
    private User user;
 
    // a UserRole refers to a Role
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_ROLE_ID, nullable = false)
    private Role role;
 
    // manufacturers
    public UserRole() {
 
    }
 
    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [hashcode]
    ...
 
    // getters and setters
...
}
  • الأسطر 34–36: تنفيذ المفتاح الخارجي من جدول [USERS_ROLES] إلى جدول [USERS
  • الأسطر 38–41: تنفيذ المفتاح الخارجي من الجدول [USERS_ROLES] إلى الجدول [ROLES

20.4.4. طبقة [repositories]

  

تدير واجهة [UserRepository] الوصول إلى كيانات [User]:


package spring.security.repositories;
 
import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
public interface UserRepository extends CrudRepository<User, Long> {
 
    // liste des rôles d'un utilisateur identifié par son id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);
 
    // liste des rôles d'un utilisateur identifié par son login unique
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);
 
    // recherche d'un utilisateur via son login
    User findUserByLogin(String login);
}
  • السطر 9: واجهة [UserRepository] تمتد واجهة Spring Data [CrudRepository] (السطر 7)؛
  • السطران 12-13: تسترد طريقة [getRoles(long id)] جميع الأدوار لمستخدم محدد بواسطة [id] الخاص به
  • السطران 16-17: كما هو مذكور أعلاه، ولكن بالنسبة لمستخدم يتم تحديده بواسطة اسم المستخدم وكلمة المرور؛
  • السطر 20: للبحث عن مستخدم باستخدام اسم تسجيل الدخول الخاص به؛

تدير واجهة [RoleRepository] الوصول إلى كيانات [Role]:


package spring.security.repositories;
 
import generic.jpa.entities.dbproduitscategories.Role;
 
import org.springframework.data.repository.CrudRepository;
 
public interface RoleRepository extends CrudRepository<Role, Long> {
 
    // search for a role by name
    Role findRoleByName(String name);
 
}
  • السطر 7: واجهة [RoleRepository] تمتد من واجهة [CrudRepository
  • السطر 10: يمكنك البحث عن دور حسب اسمه. لاحظ أن الكيان [Role] يحتوي على حقل [name]. يتم تنفيذ الطريقة [findEntityByField] تلقائيًا بواسطة Spring Data. لذلك، لا داعي لتنفيذ الطريقة [findRoleByName] هنا. ما عليك سوى إعلانها في الواجهة.

تدير واجهة [UserRoleRepository] الوصول إلى كيانات [UserRole]:


package spring.security.repositories;
 
import generic.jpa.entities.dbproduitscategories.UserRole;
 
import org.springframework.data.repository.CrudRepository;
 
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
 
}
  • السطر 7: واجهة [UserRoleRepository] تمتد ببساطة واجهة [CrudRepository] دون إضافة أي طرق جديدة؛

20.4.5. طبقة [DAO2]

  

تحتوي طبقة [DAO2] على نفس الفئات الموجودة في طبقة [DAO2] في مشروع [spring-security-server-jdbc-generic] الذي تمت مناقشته سابقًا في القسم 20.2.2. والآن، ما علينا سوى تنفيذها باستخدام الفئات الموجودة في طبقة [repositories].

تتطور فئة [AppUserDetails] على النحو التالي:


package spring.security.dao;
 
import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;
 
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.data.infrastructure.DaoException;
import spring.security.repositories.UserRepository;
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // properties
    private User user;
    private UserRepository userRepository;
    
    // local
    private String simpleClassName = getClass().getSimpleName();
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        Iterable<Role> roles;
        try {
            roles = userRepository.getRoles(user.getId());
        } catch (Exception e) {
            e.printStackTrace();
            throw new DaoException(167, e, simpleClassName);
        }
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
...
}
  • في السطر 31، يستقبل منشئ الفئة كائن [UserRepository] كمعلمة ثانية، مما يتيح للفئة استرداد أدوار مستخدم معين (السطر 42)؛

يتطور مكون Spring [AppUserDetailsService] على النحو التالي:


package spring.security.dao;
 
import generic.jpa.entities.dbproduitscategories.User;
 
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.data.infrastructure.DaoException;
import spring.security.repositories.UserRepository;
 
@Service
public class AppUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
    // local
    private String simpleClassName = getClass().getName();
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // search for user via login
        User user;
        try {
            user = userRepository.findUserByLogin(login);
        } catch (Exception e) {
            throw new DaoException(168, e, simpleClassName);
        }
        // found?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(user, userRepository);
    }
 
}
  • السطر 18: إدخال مكون Spring [userRepository]، والذي سيسمح للخدمة بإرجاع المستخدم المحدد بواسطة معلومات تسجيل الدخول الخاصة به، السطر 27؛

في النهاية، ندرك أننا نحتاج فقط إلى [userRepository] وليس إلى المستودعين الآخرين [roleRepository، userRoleRepository]. سيتم استخدامهما في المشروع التالي، الذي يهدف إلى ملء الجداول [USERS، ROLES، USERS_ROLES].

20.4.6. الاختبارات

يتم تشغيل خدمة الويب الآمنة باستخدام التكوين المسمى [spring-security-server-jpa-generic-hibernate-eclipselink] [1]. يتم تشغيل اختبار [JUnitTestDao] للعميل العام باستخدام التكوين المسمى [spring-security-client-generic-JUnitTestDao] [2]:

تم اجتياز الاختبارات.

20.5. مشروع Eclipse [spring-security-create-users]

  

20.5.1. قاعدة البيانات

يؤدي تشغيل المشروع إلى ملء الجداول [USERS، ROLES، USERS_ROLES] في قاعدة البيانات [dbproduitscategories]:

Image

 

بيانات الاعتماد التي تم إنشاؤها [اسم المستخدم/كلمة المرور] هي كما يلي: [admin/admin]، [user/user]، [guest/guest]. بشكل افتراضي، يتم تشفير كلمات المرور.

Image

20.5.2. تكوين Maven

المشروع هو مشروع 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>dvp.spring.database</groupId>
    <artifactId>spring-security-create-users-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>spring-security-create-users-jpa</name>
    <description>création de utilisateurs dans la base [dbproduitscategories]</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- Spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- spring-security-server-jpa-generic -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • الأسطر 22–25: التبعية لإطار عمل Spring Security. يوفر هذا الإطار خوارزمية تشفير كلمة المرور
  • الأسطر 27–31: الاعتماد على مشروع [spring-security-server-jpa-generic] الذي أنشأناه للتو. ينفذ هذا المشروع طبقات [repositories] و[JPA] للمشروع؛

في النهاية، تكون التبعيات كما يلي:

  

20.5.3. طبقة [وحدة التحكم]

نظرًا لأن طبقتي [repositories] و[JPA] يتم تنفيذهما بواسطة التبعية [spring-security-server-jpa-generic]، لم يتبق سوى طبقة [console] لتنفيذها.

  
  • [AppConfig] هي فئة تكوين Spring الخاصة بالمشروع؛
  • [CreateUsers] هي الفئة القابلة للتنفيذ التي تنشئ المستخدمين والأدوار؛
  • [Base64Encoder] هي فئة مساعدة لتوليد رمز Base64 لزوج [login, password]. وقد استخدمناها بالفعل. وهي غير مطلوبة في هذا المشروع؛

فيما يلي فئة التكوين Spring [AppConfig]:


package spring.security.install;
 
import generic.jpa.config.ConfigJpa;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
 
@Configuration
@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
  • السطر 10: يحدد مكان العثور على [مستودعات] التطبيق. وهي موجودة في حزمة [spring.security.repositories] التابعة للتبعية [spring-security-server-jpa-generic]
  • السطر 11: يستورد الحبوب من فئة [ConfigJpa]، التي تهيئ طبقة [JPA] للمشروع. توجد هذه الفئة في التبعية [mysql-config-jpa-hibernate]:
  

فئة [CreateUsers] هي كما يلي:


package spring.security.install;
 
import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;
import generic.jpa.entities.dbproduitscategories.UserRole;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
 
import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;
 
public class CreateUsers {
 
    public static void main(String[] args) {
 
        // end
        System.out.println("Travail en cours...");
 
        // we create three users
        String[] logins = { "admin", "user", "guest" };
        String[] passwds = { "admin", "user", "guest" };
        String[] roles = { "admin", "user", "guest" };
 
        // spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        for (int i = 0; i < logins.length; i++) {
            // we retrieve information from user n° i
            String login = logins[i];
            String password = passwds[i];
            String roleName = String.format("ROLE_%s", roles[i].toUpperCase());
            // does the role already exist?
            Role role = roleRepository.findRoleByName(roleName);
            // if it doesn't exist, we create it
            if (role == null) {
                role = roleRepository.save(new Role(roleName));
            }
            // does the user already exist?
            User user = userRepository.findUserByLogin(login);
            // if it doesn't exist, we create it
            if (user == null) {
                // hash the password with bcrypt
                String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
                // save user
                user = userRepository.save(new User(null, null, login, login, crypt));
                // we create the relationship with the role
                userRoleRepository.save(new UserRole(user, role));
            } else {
                // the user already exists - does he/she have the required role?
                boolean trouvé = false;
                for (Role r : userRepository.getRoles(user.getId())) {
                    if (r.getName().equals(roleName)) {
                        trouvé = true;
                        break;
                    }
                }
                // if not found, we create the relationship with the role
                if (!trouvé) {
                    userRoleRepository.save(new UserRole(user, role));
                }
            }
        }
        // closing Spring context
        context.close();
        // end
        System.out.println("Travail terminé...");
    }
 
}
  • الأسطر 22–24: تعريف اسم المستخدم وكلمة المرور والدور لثلاثة مستخدمين؛
  • السطر 27: يتم إنشاء سياق Spring من فئة التكوين [AppConfig
  • الأسطر 28–30: استرداد المراجع إلى كائنات [Repository] الثلاثة التي يمكن استخدامها لإنشاء مستخدم؛
  • السطر 31: إنشاء المستخدمين الثلاثة؛
  • الأسطر 33-35: معلومات لإنشاء المستخدم #i؛
  • السطر 37: نتحقق مما إذا كان الدور موجودًا بالفعل؛
  • الأسطر 39-41: إذا لم يكن موجودًا، نقوم بإنشائه في قاعدة البيانات. سيكون اسمه على شكل [ROLE_XX
  • السطر 43: نتحقق مما إذا كان اسم المستخدم موجودًا بالفعل؛
  • الأسطر 45-52: إذا لم يكن اسم المستخدم موجودًا، نقوم بإنشائه في قاعدة البيانات؛
  • السطر 47: نقوم بتشفير كلمة المرور. هنا، نستخدم فئة [BCrypt] من Spring Security (السطر 8). لذلك نحتاج إلى أرشيفات هذا الإطار. يتضمن ملف [pom.xml] هذه التبعية:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • السطر 49: يتم حفظ المستخدم في قاعدة البيانات؛
  • السطر 51: وكذلك العلاقة التي تربطه بدوره؛
  • الأسطر 55-60: إذا كان تسجيل الدخول موجودًا بالفعل، نتحقق مما إذا كان الدور الذي نريد تعيينه له موجودًا بالفعل ضمن أدواره؛
  • الأسطر 62–64: إذا لم يتم العثور على الدور الذي يتم البحث عنه، يتم إنشاء صف في جدول [USERS_ROLES] لربط المستخدم بدوره؛
  • لم نقم بالحماية من الاستثناءات المحتملة. هذه فئة مساعدة لإنشاء المستخدمين بسرعة؛

لتشغيل المشروع، قم بتنفيذ تكوين التشغيل المسمى [spring-security-create-users-hibernate-eclipselink]:

 

لقد أنشأنا للتو خدمتين ويب آمنتين:

  • إحداهما تعتمد على بنية [الأمان / الويب / JDBC / MySQL
  • والآخر يعتمد على بنية [أمنية / ويب / Hibernate / MySQL

سنلقي الآن نظرة على بنيتين أخريين:

  • بنية [الأمان / الويب / EclipseLink / SQL Server 2014 Express
  • بنية [الأمان / الويب / OpenJpa / Oracle Express

  • في [1]، نقوم بتحميل المشاريع التي تهيئ طبقة [JDBC / SQL Server] وطبقة [JPA / EclipseLink / SQL Server

ملاحظة: اضغط على Alt-F5، ثم أعد إنشاء جميع مشاريع Maven.

نفترض أن نظام إدارة قواعد البيانات SQL Server قيد التشغيل وأن قاعدة البيانات [dbproduitscategories] قد تم إنشاؤها. أولاً، نحتاج إلى ملء الجداول [USERS، ROLES، USERS_ROLES] في قاعدة البيانات هذه. للقيام بذلك، قم بتشغيل تكوين التنفيذ المسمى [spring-security-create-users-hibernate-eclipselink]:

من المفترض أن يؤدي ذلك إلى ملء الجداول الثلاثة بالبيانات:

 
 
  • ابدأ تشغيل خدمة الويب الآمنة باستخدام التكوين المسمى [spring-security-server-jpa-generic-hibernate-eclipselink][1]؛
  • قم بتشغيل اختبار JUnitTestDao باستخدام التكوين المسمى [spring-security-client-generic-JUnitTestDao][2]. يجب أن يجتاز الاختبار [3]؛

20.5.5. الهندسة [الأمن / الويب / OpenJpa / Oracle Express]

  • في [1]، قم بتحميل المشاريع التي تهيئ طبقة [JDBC / Oracle Express] وطبقة [JPA / OpenJpa / Oracle Express

ملاحظة: اضغط على Alt-F5، ثم أعد إنشاء جميع مشاريع Maven.

نفترض أن نظام إدارة قواعد البيانات Oracle Express قيد التشغيل وأن قاعدة البيانات [dbproduitscategories] قد تم إنشاؤها. أولاً، نحتاج إلى ملء الجداول [USERS، ROLES، USERS_ROLES] في قاعدة البيانات هذه. للقيام بذلك، قم بتشغيل تكوين التنفيذ المسمى [spring-security-create-users-openjpa]:

من المفترض أن يؤدي ذلك إلى ملء الجداول الثلاثة بالبيانات:

 
 
  • قم بتشغيل خدمة الويب الآمنة باستخدام التكوين المسمى [spring-security-server-jpa-generic-openjpa][1-2]؛
  • قم بتشغيل اختبار JUnitTestDao باستخدام التكوين المسمى [spring-security-client-generic-JUnitTestDao][3]. يجب أن يجتاز الاختبار [4]؛