Skip to content

20. Захист веб-сервісу доступу до бази даних [dbproduitscategories]

20.1. Налаштування робочого середовища

Ми забезпечимо безпеку веб-сервісу за допомогою таких проектів:

  
  • проекти [spring-security-*] знаходяться у папці [<exemples>\spring-database-generic\spring-security];
  • захист буде налаштовано для SGBD та MySQL із використанням шару [DAO / JDBC], а потім шару [DAO / JPA / Hibernate];
  • натисніть Alt-F5, а потім перегенеруйте всі проекти Maven;

Нам потрібно створити користувачів у базі даних [dbproduitscategories]. Для цього скористайтеся конфігурацією виконання [spring-security-create-users-hibernate-eclipselink]:

Виконання цієї конфігурації заповнює таблиці [USERS, ROLES, USERS_ROLES] з таблиці [dbproduitscategories]:

 

Створені ідентифікатори [login/passwd] такі: [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], який керує таблицями [PRODUITS] та [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>
        <!-- веб-сервер / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jdbc-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- плагіни -->
    <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], який керує таблицями [PRODUITS] та [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;
    // властивості
    private User user;
    private String simpleClassName = getClass().getSimpleName();

    // конструктори
    public AppUserDetails() {
    }

    public AppUserDetails(User user, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.user = user;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }

    // -------------------------інтерфейс
    @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;
    }
...

    // приватні методи----------------
    private List<Role> getRoles(Long id) {
        try {
            // пошук користувача за його ідентифікатором
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ROLES_BYUSERID, Collections.singletonMap("id", id),    new ShortRoleMapper());
        } catch (Exception e) {
            //e.printStackTrace();
            throw new DaoException(167, e, simpleClassName);
        }
    }

}

// --------------------- маппери
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;
    // властивості
    private User user;
    private String simpleClassName = getClass().getSimpleName();

    // конструктори
    public AppUserDetails() {
    }

    public AppUserDetails(User user, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.user = user;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }

    // -------------------------інтерфейс
    @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;
    }

    // методи отримання та встановлення
    ...
}
  • рядки 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 {

    // ін'єкції
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    // локальний
    private String simpleClassName = getClass().getSimpleName();

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        List<User> users;
        try {
            // пошук користувача за логіном
            users = namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_USER_BYLOGIN,
                    Collections.singletonMap("login", login), new ShortUserMapper());
        } catch (Exception e) {
            throw new DaoException(145, e, simpleClassName);
        }
        // знайдено?
        if (users.size() == 0) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // повертаємо дані користувача
        return new AppUserDetails(users.get(0), namedParameterJdbcTemplate);
    }
}

// --------------------- маппери
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 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]:

  

Саме цей проект реалізує рівень [web]. Його не потрібно змінювати.

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;

    // захист
    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) {
            // пароль передається у заголовку 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);
        }
    }
}
  • рядок 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 (Cross Site Request Forgery), який користувач, що бажає пройти автентифікацію, повинен надіслати серверу. Тут цей механізм вимкнено. Це, разом із булевим значенням (isSecured=false), дозволяє використовувати веб-додаток без заходів безпеки;
  • рядок 42: увімкнено режим автентифікації за допомогою заголовка HTTP. Клієнт повинен надіслати такий заголовок HTTP:
Authorization:Basic code

де code — це кодування рядка login:password за алгоритмом Base64. Наприклад, 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) {
        // очікуються два аргументи: логін та пароль
        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=

Тепер ми готові до тестування:

  • необхідно запустити SGBD та MySQL;
  • заповнюємо таблиці [PRODUITS] та [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 / user. Він існує, але не має доступу до веб-сервісу. Якщо запустити програму кодування Base64 з двома аргументами [user user]:

  

отримаємо такий результат:

dXNlcjp1c2Vy
  • у [1]: заголовок HTTP з помилковою автентифікацією;
  • в [2]: відповідь веб-сервісу. Вона відрізняється від попередньої, яка була [401 Unauthorized]. Цього разу користувач успішно пройшов аутентифікацію, але не має достатніх прав для доступу до 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 вдалося здійснити без змін у вихідному веб-проєкті / json. Цей дуже сприятливий випадок зумовлений тим, що три таблиці, додані до бази даних, є незалежними від існуючих таблиць. Їх навіть можна було б розмістити в окремій базі даних. В інших випадках додані таблиці можуть мати зв’язки з існуючими таблицями. У такому разі код існуючого шару [DAO] потребує перегляду.

20.3. Клієнт, запрограмований для захищеного веб-сервісу / jSON

Ми вже написали клієнт для незахищеного веб-сервісу / jSON:

Тепер ми створимо клієнт, налаштований для захищеного веб-сервісу:

  

20.3.1. Рівень [Client 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 {

    // властивості
    private String login;
    private String password;

    // конструктор
    public Credentials() {
    }

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

    // методи getter та setter
...
}

Клас [Client], який реалізує інтерфейс [IClient], змінюється наступним чином:


package spring.security.client.dao;

...

@Component
public class Client implements IClient {

    // вставки
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // локальні
    private String simpleClassName = getClass().getSimpleName();

    private String getBase64(Credentials credentials) {
        // кодування імені користувача та пароля у форматі Base64 — потрібно 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())));
    }

    // загальний запит
    @Override
    public <T1, T2> T1 getResponse(Credentials credentials, String url, HttpMethod method, int errStatus, T2 body) {
        // відповідь сервера
        ResponseEntity<Response<T1>> response;
        try {
            // підготовка запиту
            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);
            }
            // виконується запит
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // інкапсуляція винятку
            throw new DaoException(errStatus, e, simpleClassName);
        }
...
    }
...
}
  • рядки 33–35, 40–42: якщо користувач [credentials] не дорівнює null, то додається заголовок автентифікації. Кодування Base64 імені користувача та його пароля забезпечується методом [getBase64] у рядках 17–21. Слід звернути увагу на те, що цей метод використовує клас [Base64], який належить до JDK 1.8. Наш клієнт HTTP може працювати з незахищеним веб-сервісом. Достатньо передати йому [credentials], що дорівнює null;
  • крім наведених вище рядків, код залишається незмінним;

20.3.2. Рівень [DAO]

20.3.2.1. Інтерфейс [IDao]

  

Усі методи інтерфейсу [IDao] проекту [spring-webjson-client-generic] отримують додатковий параметр [Credentials 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 {

    // список усіх сутностей T
    public List<T> getAllShortEntities(Credentials credentials);

    public List<T> getAllLongEntities(Credentials credentials);

    // окремих сутностей — коротка версія
    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);

    // окремих об’єктів — розширена версія
    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);

    // оновлення декількох об’єктів
    public List<T> saveEntities(Credentials credentials, Iterable<T> entities);

    public List<T> saveEntities(Credentials credentials, @SuppressWarnings("unchecked") T... entities);

    // видалення всіх об’єктів
    public void deleteAllEntities(Credentials credentials);

    // видалення декількох об’єктів
    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 {
    // аутентифікація
    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 credentials], який вони передають до дочірнього класу. Ось приклад:


    @Override
    public List<T1> getShortEntitiesById(Credentials credentials, Iterable<Long> ids) {
        // правильність аргументу
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // результат
        return getShortEntitiesById(credentials, Lists.newArrayList(ids));
}
  • метод [getShortEntitiesById] отримує параметр [Credentials 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;

    // перевірка [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. Принцип його роботи полягає в тому, що виклик завершується винятком, якщо користувач [credentials] є невідомим або не має достатніх прав;

Клас [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> {

    // ін'єкції
    @Autowired
    protected ApplicationContext context;
    @Autowired
    protected IClient client;

    @Override
    public List<Categorie> getAllShortEntities(Credentials credentials) {
        try {
            // фільтри jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // отримати всі категорії
            Object map = client.<List<Categorie>, Void> getResponse(credentials, "/getAllShortCategories", HttpMethod.GET,
                    202, null);
            // список категорій 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 {

    // шар [DAO]
    @Autowired
    private IAuthenticate authenticate;

    // користувачі
    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
    @Autowired
    private ApplicationContext context;
    // шар [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

....

    // користувачі
    static private Credentials admin;

    @BeforeClass
    public static void init() {
        admin = new Credentials("admin", "admin");
    }

    @Before
    public void clean() {
        // перед кожним тестом очищаємо базу даних
        log("Vidage de la base de données", 1);
        // очищаємо таблицю [CATEGORIES] і, відповідно, таблицю [PRODUITS]
        daoCategorie.deleteAllEntities(admin);
        // очищаються словники
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }

    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // заповнюються таблиці
        ...
        // додавання категорії — каскадно також будуть
        // додані — одночасно виводимо результат
        return daoCategorie.saveEntities(admin, categories);
    }

    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // список категорій
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities(admin);
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // список товарів
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities(admin);
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // результат
        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();
        // захищений додаток?
        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);
        }
}

Рядок 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], який керує таблицями [PRODUITS] та [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>
        <!-- веб-сервер / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- плагіни -->
    <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: залежність від шару [security] у проєкті;
  • рядки 29–33: залежність від шару [web] проекту. Проект [spring-webjson-server-jpa-generic] повністю реалізує шар [web]. Цей шар не потрібно писати чи змінювати;

У підсумку залежності є такими:

  

20.4.2. Конфігурація Spring

  

Підійде файл конфігурації [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 {
    // властивості
    @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;

    // властивості
    @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;

    // пов’язані UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;

    // конструктори
    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;
    }

    // ------------------------------------------------------------
    // перевизначення [equals] та [hashcode]
...

    // методи getter та setter
...
}
  • рядок 23: клас реалізує інтерфейс [AbstractCoreEntity], який вже використовувався для інших сутностей;
  • рядки 34–35: тип об’єкта. Ця властивість не зберігається в базі даних [@Transient];
  • рядки 38–43: три основні властивості користувача (name, login, password);
  • рядки 46–48: список ролей користувача. Їх може бути кілька. Так само ми побачимо, що з однією роллю може бути пов’язано кілька користувачів. Отже, у значенні терміна JPA, між сутностями [User] та [Role] існує відношення [ManyToMany]:
    • один користувач може бути пов’язаний з кількома ролями;
    • роль може бути пов’язана з кількома користувачами;

Цей зв’язок [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 {
    // властивості
    @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;

    // властивості
    @Column(name = ConfigJdbc.TAB_ROLES_NAME, length = 30, unique = true, nullable = false)
    private String name;

    // пов’язані UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "role", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;

    // конструктори
    public Role() {
    }

    public Role(Long id, Long version, String name) {
        this.id = id;
        this.version = version;
        this.name = name;
    }

    // методи getter та setter
    public Role(String name) {
        this.name = name;
    }

    // ------------------------------------------------------------
    // перевизначення [equals] та [hashcode]
...
    // методи getter та setter
...
}
  • рядки 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 {
    // властивості
    @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;

    // UserRole посилається на User
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_USER_ID, nullable = false)
    private User user;

    // UserRole посилається на Role
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_ROLE_ID, nullable = false)
    private Role role;

    // конструктори
    public UserRole() {

    }

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

    // ------------------------------------------------------------
    // перевизначення [equals] та [hashcode]
    ...

    // методи getter та setter
...
}
  • рядки 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> {

    // список ролей користувача, ідентифікованого за його ідентифікатором
    @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);
}
  • рядок 9: інтерфейс [UserRepository] розширює інтерфейс [CrudRepository] з Spring Data (рядок 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> {

    // пошук ролі за її назвою
    Role findRoleByName(String name);

}
  • рядок 7: інтерфейс [RoleRepository] розширює інтерфейс [CrudRepository];
  • рядок 10: можна шукати роль за її назвою. Нагадаємо, що суть [Role] має поле [name]. Метод [findEntityByChamp] автоматично реалізується Spring Data. Тому немає необхідності реалізовувати тут метод [finRoleByName]. Потрібно лише оголосити його в інтерфейсі.

Інтерфейс [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;

    // властивості
    private User user;
    private UserRepository userRepository;
    
    // локальний
    private String simpleClassName = getClass().getSimpleName();

    // конструктори
    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<>();
        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;
    // локальний
    private String simpleClassName = getClass().getName();

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // пошук користувача за логіном
        User user;
        try {
            user = userRepository.findUserByLogin(login);
        } catch (Exception e) {
            throw new DaoException(168, e, simpleClassName);
        }
        // знайдено?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // подаємо детальну інформацію про користувача
        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

 

Створені ідентифікатори [login/passwd] такі: [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. Рівень [console]

Оскільки шари [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: вказується, де знайти [repositories] у додатку. Вони знаходяться в пакеті [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) {

        // кінець
        System.out.println("Travail en cours...");

        // створюємо трьох користувачів
        String[] logins = { "admin", "user", "guest" };
        String[] passwds = { "admin", "user", "guest" };
        String[] roles = { "admin", "user", "guest" };

        // контекст Spring
        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++) {
            // отримання інформації про користувача № i
            String login = logins[i];
            String password = passwds[i];
            String roleName = String.format("ROLE_%s", roles[i].toUpperCase());
            // чи існує ця роль?
            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(null, null, 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é...");
    }

}
  • рядки 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]:

 

Ми щойно створили два захищені веб-сервіси:

  • один із архітектурою [security / web / JDBC / MySQL];
  • інший — з архітектурою [security / web / Hibernate / MySQL];

Тепер розглянемо ще дві архітектури:

  • архітектуру [security / web / EclipseLink / SQL Server 2014 Express];
  • архітектуру [security / web / OpenJpa / Oracle Express];

  • у [1] завантажуються проекти, що конфігурують шар [JDBC / SQL Server] та шар [JPA / EclipseLink / SQL Server];

Примітка: натисніть Alt-F5, а потім перегенеруйте всі проекти Maven.

Припускаємо, що сервер SGBD SQL запущено, а база даних [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. Архітектура [security / web / OpenJpa / Oracle Express]

  • у [1] завантажуються проекти, що конфігурують шар [JDBC / Oracle Express] та шар [JPA / OpenJpa / Oracle Express];

Примітка: натисніть Alt-F5, а потім перегенеруйте всі проекти Maven.

Припускаємо, що Oracle Express (SGBD) запущено і база даних [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];