Skip to content

20. Zabezpieczenie serwisu internetowego zapewniającego dostęp do bazy danych [dbproduitscategories]

20.1. Konfiguracja środowiska pracy

Zabezpieczymy serwis internetowy za pomocą następujących projektów:

  
  • projekty [spring-security-*] znajdują się w folderze [<exemples>\spring-database-generic\spring-security];
  • zabezpieczenia zostaną wdrożone dla projektów SGBD i MySQL z wykorzystaniem warstwy [DAO / JDBC], a następnie warstwy [DAO / JPA / Hibernate];
  • naciśnij Alt-F5, a następnie zregeneruj wszystkie projekty Maven;

Musimy utworzyć użytkowników w bazie danych [dbproduitscategories]. W tym celu należy użyć konfiguracji uruchomienia [spring-security-create-users-hibernate-eclipselink]:

Uruchomienie tej konfiguracji wypełnia tabele [USERS, ROLES, USERS_ROLES] danymi z tabeli [dbproduitscategories]:

 

Utworzone identyfikatory [login/passwd] to: [admin/admin], [user/user], [guest/guest]. W bazie danych hasła są zaszyfrowane.

Image

Po wykonaniu tych czynności uruchom konfigurację uruchomieniową o nazwie [spring-security-server-jpa-generic-hibernate-eclipselink], która uruchamia zabezpieczoną usługę internetową (należy uruchomić MySQL):

Następnie uruchom konfigurację o nazwie [spring-security-client-generic-JUnitTestDao], która testuje zabezpieczoną usługę internetową:

Test powinien zakończyć się powodzeniem.

20.2. Projekt Eclipse [spring-security-server-jdbc-generic]

Zabezpieczona usługa internetowa jest zaimplementowana w projekcie [spring-security-server-jdbc-generic]:

Powyżej:

  • warstwa [DAO1] to warstwa [DAO], która zarządza tabelami [PRODUITS] i [CATEGORIES] w bazie danych [dbproduitscategories]. Została już napisana;
  • warstwa [DAO2] to warstwa [DAO], która zarządza tabelami [USERS], [ROLES] oraz [USERS_ROLES] z bazy [dbproduitscategories]. Pozostaje ją jeszcze utworzyć;

  

20.2.1. Konfiguracja Maven

Projekt [spring-security-server-jdbc-generic] jest projektem Maven skonfigurowanym przez następujący plik [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>
        <!-- serwer WWW / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jdbc-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- wtyczki -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • wiersze 29–33: wykorzystujemy istniejące ustawienia wraz z archiwum analizowanej usługi internetowej / json / jdbc;
  • wiersze 24–27: zależność, która wprowadza klasy Spring Security;

Ostatecznie projekt zawiera następujące zależności od innych projektów załadowanych w Eclipse:

  

20.2.2. Warstwa [DAO2]

Powyżej:

  • warstwa [DAO1] to warstwa [DAO], która zarządza tabelami [PRODUITS] i [CATEGORIES] w bazie [dbproduitscategories]. Została już napisana;
  • warstwa [DAO2] to warstwa [DAO], która zarządza tabelami [USERS], [ROLES] oraz [USERS_ROLES] z bazy [dbproduitscategories]. To właśnie tę warstwę zamierzamy teraz utworzyć;
  

Spring Security wymaga utworzenia klasy implementującej następujący interfejs [UsersDetail]:

 

Interfejs ten został zaimplementowany przez klasę [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;
    // właściwości
    private User user;
    private String simpleClassName = getClass().getSimpleName();

    // konstruktory
    public AppUserDetails() {
    }

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

    // -------------------------interfejs
    @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;
    }
...

    // metody prywatne----------------
    private List<Role> getRoles(Long id) {
        try {
            // wyszukiwanie użytkownika na podstawie jego identyfikatora
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ROLES_BYUSERID, Collections.singletonMap("id", id),    new ShortRoleMapper());
        } catch (Exception e) {
            //e.printStackTrace();
            throw new DaoException(167, e, simpleClassName);
        }
    }

}

// --------------------- mapery
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"));
    }
}
  • wiersz 22: klasa [AppUserDetails] implementuje interfejs [UserDetails];
  • wiersze 29–30: klasa ta hermetyzuje użytkownika (wiersz 19) oraz repozytorium, które umożliwia uzyskanie szczegółowych informacji o tym użytkowniku (wiersz 20);
  • wiersz 27: dostęp do bazy danych będzie realizowany za pośrednictwem klasy JDBC przy użyciu obiektu [NamedParameterJdbcTemplate namedParameterJdbcTemplate] zdefiniowanego w projekcie [spring-jdbc-generic-04]. Należy zauważyć, że obiekt ten nie jest wstrzykiwany przez Spring, jak to często miało miejsce. Jest on przekazywany do konstruktora w wierszach 36–39. Dlaczego? Ponieważ klasa [AppUserDetails] nie jest komponentem Spring (brak adnotacji @Component), a zatem nie można w niej dokonywać wstrzyknięć;
  • wiersze 36–39: konstruktor, który tworzy instancję klasy z użytkownikiem i jego repozytorium;
  • wiersze 42–49: implementacja metody [getAuthorities] z interfejsu [UserDetails]. Musi ona utworzyć kolekcję elementów typu [GrantedAuthority] lub pochodnego. W tym przypadku używamy typu pochodnego [SimpleGrantedAuthority] (wiersz 46), który zawiera nazwę jednej z ról użytkownika z wiersza 29;
  • wiersze 45–47: przeglądamy listę ról użytkownika z wiersza 29, aby utworzyć listę elementów typu [SimpleGrantedAuthority];
  • wiersz 45: w celu uzyskania ról użytkownika wykorzystuje się metodę prywatną [getRoles] z wiersza 53;
  • wiersz 56: wykonuje następujące polecenie SQL [ConfigJdbc.SELECT_ROLES_BYUSERID] (zdefiniowane w [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";

To zapytanie SQL wykonuje połączenie między trzema tabelami [USERS, ROLES, USERS_ROLES] w celu uzyskania ról użytkownika zidentyfikowanego na podstawie jego klucza głównego. Jest ono skonfigurowane przy użyciu klucza głównego [:id] użytkownika, dla którego wyszukiwane są role.

  • wiersz 56: każdy wiersz wynikowy zapytania [SELECT] jest przekształcany w entytę [Role] przez klasę [ShortRowMapper] w wierszach 66–72;

Wróćmy do kodu klasy [AppUserDetails]:


package spring.security.dao;

...

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // JdbcTemplate
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    // właściwości
    private User user;
    private String simpleClassName = getClass().getSimpleName();

    // konstruktorzy
    public AppUserDetails() {
    }

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

    // -------------------------interfejs
    @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;
    }

    // metody pobierające i ustawiające
    ...
}
  • wiersze 35–37: implementują metodę [getPassword] z interfejsu [UserDetails]. Zwracane jest hasło użytkownika z wiersza 12;
  • wiersze 39–42: implementują metodę [getUserName] z interfejsu [UserDetails]. Zwracana jest nazwa użytkownika z wiersza 12;
  • wiersze 44–47: konto użytkownika nigdy nie wygasa;
  • wiersze 49–52: konto użytkownika nigdy nie zostaje zablokowane;
  • wiersze 54–57: dane logowania użytkownika nigdy nie tracą ważności;
  • wiersze 59–62: konto użytkownika jest zawsze aktywne;

Spring Security wymaga również istnienia klasy implementującej interfejs [AppUserDetailsService]:

 

Interfejs ten jest implementowany przez następującą klasę [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 {

    // wstrzyknięcia
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    // lokalne
    private String simpleClassName = getClass().getSimpleName();

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        List<User> users;
        try {
            // wyszukiwanie użytkownika na podstawie loginu
            users = namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_USER_BYLOGIN,
                    Collections.singletonMap("login", login), new ShortUserMapper());
        } catch (Exception e) {
            throw new DaoException(145, e, simpleClassName);
        }
        // znaleziono?
        if (users.size() == 0) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // zwracamy szczegóły użytkownika
        return new AppUserDetails(users.get(0), namedParameterJdbcTemplate);
    }
}

// --------------------- mapery
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"));
    }
}
  • wiersz 21: klasa będzie komponentem Spring;
  • wiersze 25–26: dostęp do bazy danych będzie realizowany za pośrednictwem klasy JDBC przy użyciu obiektu [NamedParameterJdbcTemplate namedParameterJdbcTemplate] zdefiniowanego w beanach projektu [spring-jdbc-generic-04];
  • wiersze 31–49: implementacja metody [loadUserByUsername] z interfejsu [UserDetailsService] (wiersz 22). Parametrem jest login użytkownika;
  • wiersze 36–37: użytkownik jest wyszukiwany na podstawie nazwy użytkownika. Kolejność SQL [ConfigJdbc.SELECT_USER_BYLOGIN] jest następująca:

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

Każdy wiersz zwrócony przez SELECT jest przekształcany w encję [User] przez klasę [ShortUserMapper] w wierszach 52–58.

  • wiersze 42–44: jeśli nie zostanie znaleziony, zgłaszany jest wyjątek;
  • wiersz 46: tworzony i zwracany jest obiekt typu [AppUserDetails]. Jest on rzeczywiście typu [UserDetails] (wiersz 32). Do jego konstruktora przekazywane są dwie informacje:
    • znalezionego użytkownika;
    • obiekt [namedParameterJdbcTemplate], który umożliwi klasie [AppUserDetails] wysłanie zapytania do bazy danych;

20.2.3. Warstwa [web]

Projekt [spring-security-server-jdbc-generic] jest zależny od projektu [spring-webjson-server-jdbc-generic]:

  

To właśnie ten projekt implementuje warstwę [web]. Nie wymaga ona modyfikacji.

20.2.4. Konfiguracja zabezpieczeń projektu

Projekt jest skonfigurowany przez następującą klasę [AppConfig]:

1
  

Spotkaliśmy się już z klasą konfiguracyjną Spring Security (patrz paragraf 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");
    }
}

Postępujemy w ten sam sposób:

  • wiersz 11: zdefiniujmy klasę rozszerzającą klasę [WebSecurityConfigurerAdapter];
  • wiersz 13: zdefiniowanie metody [configure(HttpSecurity http)], która określa uprawnienia dostępu do poszczególnych URL serwisu internetowego;
  • wiersz 19: zdefiniowanie metody [configure(AuthenticationManagerBuilder auth)], która określa użytkowników i ich role;

Klasa [AppConfig] będzie wyglądać następująco:


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;

    // zabezpieczenie
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // uwierzytelnianie odbywa się za pomocą komponentu [appUserDetailsService]
        // hasło jest szyfrowane za pomocą algorytmu skrótu BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // czy aplikacja jest bezpieczna?
        if (activateSecurity) {
            // hasło jest przesyłane w nagłówku Authorization: Basic xxxx
            http.httpBasic();
            // metoda HTTP OPTIONS musi być autoryzowana dla wszystkich
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // tylko rola ADMIN może korzystać z aplikacji
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // wszystkie sesje URL
                    .hasRole("ADMIN");
            // sesja czy nie?
            //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • wiersz 17: klasa ta jest klasą konfiguracyjną Spring;
  • wiersz 18: aktywuje elementy Spring Security;
  • wiersz 26: pobieramy komponenty Spring z warstwy [DAO2] oraz z pakietu [spring.security.service], o którym będziemy mówić później;
  • wiersz 23: importujemy bean’y z projektu [spring-webjson-server-jdbc-generic], który implementuje warstwę [web]. Wśród tych bean’ów znajdują się również te z warstwy [DAO1];
  • wiersze 22–23: wstrzykiwana jest klasa [AppUserDetails], która zapewnia użytkownikom dostęp do aplikacji;
  • wiersz 26: wartość logiczna określająca, czy aplikacja internetowa jest zabezpieczona (true), czy nie (false);
  • wiersze 28–33: metoda [configure(HttpSecurity http)] definiuje użytkowników i ich role. Jako parametr przyjmuje typ [AuthenticationManagerBuilder]. Parametr ten jest uzupełniany o dwie informacje (wiersz 32):
    • odwołanie do usługi [appUserDetailsService] z wiersza 23, która zapewnia dostęp zarejestrowanym użytkownikom. Należy tutaj zauważyć, że nie pojawia się informacja o tym, że są oni zarejestrowani w bazie danych. Mogą więc znajdować się w pamięci podręcznej, być dostarczani przez usługę internetową itp.
    • rodzaj szyfrowania zastosowanego dla hasła. Zastosowaliśmy algorytm BCrypt;
  • wiersze 35–53: metoda [configure(HttpSecurity http)] definiuje uprawnienia dostępu do URL serwisu internetowego;
  • wiersz 38: w projekcie wprowadzającym widzieliśmy, że domyślnie Spring Security zarządza tokenem CSRF (Cross Site Request Forgery), który użytkownik chcący się uwierzytelnić musiał odesłać do serwera. W tym przypadku mechanizm ten jest wyłączony. W połączeniu z wartością logiczną (isSecured=false) pozwala to na korzystanie z aplikacji internetowej bez zabezpieczeń;
  • wiersz 42: włączamy tryb uwierzytelniania za pomocą nagłówka HTTP. Klient będzie musiał wysłać następujący nagłówek HTTP:
Authorization:Basic code

gdzie „code” to kodowanie ciągu „login:password” za pomocą algorytmu Base64. Na przykład kodowanie Base64 ciągu admin:admin to YWRtaW46YWRtaW4=. Zatem użytkownik o nazwie logowania [admin] i haśle [admin] wyśle następujący nagłówek HTTP w celu uwierzytelnienia się:

Authorization:Basic YWRtaW46YWRtaW4=
  • wiersze 47–49: wskazują, że wszystkie elementy URL serwisu internetowego są dostępne dla użytkowników posiadających rolę [ROLE_ADMIN]. Oznacza to, że użytkownik nieposiadający tej roli nie może uzyskać dostępu do serwisu internetowego;
  • wiersz 51: w trybie [session] użytkownik, który uwierzytelnił się raz, nie musi tego robić przy kolejnych dostępach. Jest to wartość domyślna w Spring Security. Wiersz 51 wyłącza ten tryb. Jeśli jest aktywny, użytkownik będzie musiał uwierzytelnić się przy każdym dostępie. Bez sesji responsywność zabezpieczonej usługi internetowej jest mniejsza niż z sesją, dlatego wiersz 51 został skomentowany;

20.2.5. Testowanie zabezpieczonej usługi internetowej

Przetestujemy serwis internetowy przy użyciu klienta Chrome o nazwie [Advanced Rest Client]. Będziemy musieli podać nagłówek uwierzytelniający HTTP:

Authorization:Basic code

gdzie [code] to kod Base64 ciągu znaków [login:password]. Aby wygenerować ten kod, można skorzystać z następującego programu z projektu [spring-security-create-users]:

  

package spring.security.helpers;

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

public class Base64Encoder {

    public static void main(String[] args) {
        // oczekuje dwóch argumentów: login i hasło
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // pobieramy oba argumenty
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // koduje się ciąg znaków
        byte[] data = Base64.encode(chaîne.getBytes());
        // wyświetla się jej kodowanie Base64
        System.out.println(new String(data));
    }

}

Jeśli uruchomimy ten program z dwoma argumentami [admin admin]:

  

otrzymujemy następujący wynik:

YWRtaW46YWRtaW4=

Jesteśmy teraz gotowi do przeprowadzenia testów:

  • należy uruchomić SGBD oraz MySQL;
  • wypełniamy tabele [PRODUITS] i [CATEGORIES] konfiguracją wykonania o nazwie [spring-jdbc-generic-04-fillDataBase]:
 
  • jeśli nie zostało to jeszcze zrobione, uzupełniamy tabele [USERS, ROLES, USERS_ROLES] konfiguracją wykonania o nazwie [spring-security-create-users-hibernate-eclipselink]:
 
  • uruchamiamy zabezpieczoną usługę internetową przy użyciu konfiguracji wykonawczej o nazwie [spring-security-server-jdbc-generic]:
 

Następnie za pomocą klienta Chrome o nazwie [Advanced Rest Client] żądamy pełnej wersji wszystkich kategorii:

  • w [1] żądamy URL dla długich kategorii;
  • w [2], przy użyciu metody GET;
  • w [3] podajemy nagłówek HTTP dotyczący uwierzytelniania. Kod [YWRtaW46YWRtaW4=] to kodowanie Base64 ciągu znaków [admin:admin];
  • w [4] wysyłamy polecenie HTTP;

Odpowiedź serwera jest następująca:

  • w [1] znajduje się nagłówek uwierzytelniający HTTP;
  • w [2] serwer zwraca odpowiedź jSON;

Otrzymujemy listę kategorii:

 

Spróbujmy teraz wysłać żądanie HTTP z nieprawidłowym nagłówkiem uwierzytelniającym. Odpowiedź jest wówczas następująca:

  • w [1]: nagłówek uwierzytelniający HTTP;

Otrzymujemy następującą odpowiedź:

  • w [2]: odpowiedź serwisu internetowego;

Teraz spróbujmy z użytkownikiem user / user. Użytkownik ten istnieje, ale nie ma dostępu do serwisu internetowego. Jeśli uruchomimy program do kodowania Base64 z dwoma argumentami [user user]:

  

otrzymujemy następujący wynik:

dXNlcjp1c2Vy
  • w [1]: nieprawidłowy nagłówek uwierzytelniający HTTP;
  • w [2]: odpowiedź serwisu internetowego. Różni się ona od poprzedniej, która miała postać [401 Unauthorized]. Tym razem użytkownik uwierzytelnił się poprawnie, ale nie posiada wystarczających uprawnień, aby uzyskać dostęp do URL;

Zabezpieczona usługa internetowa działa już poprawnie.

20.2.6. URL – uwierzytelnienie

  

Stworzymy URL, który pozwoli nam sprawdzić, czy użytkownik ma uprawnienia do dostępu do serwisu internetowego. W tym celu tworzymy następujący nowy kontroler 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);
    }

}
  • wiersz 9: klasa [AuthenticateController] jest kontrolerem Spring. W związku z tym udostępnia ona URL. Adnotacja [@RestController] wskazuje, że metody obsługujące te URL same zwracają odpowiedź do klienta;
  • wiersz 11: wyświetla obiekty URL i [/authenticate];
  • wiersze 12–14: metoda zwraca po prostu pusty obiekt [Response], ale z wartością [status] równą 0, co wskazuje, że nie wystąpił żaden błąd;

Do czego służy ten parametr URL? Będziemy go żądać, gdy będziemy chcieli po prostu uwierzytelnić użytkownika. Widzieliśmy już, że jeśli warstwa bezpieczeństwa nie zaakceptuje tego użytkownika, zwraca wyjątek. Oto przykład;

W przypadku użytkownika [admin:admin]:

Otrzymujemy pustą odpowiedź, ale nie pojawia się wyjątek.

W przypadku użytkownika [user:user]:

Wystąpił wyjątek.

20.2.7. Wniosek

Dodanie klas niezbędnych dla Spring Security było możliwe bez wprowadzania zmian w oryginalnym projekcie web / json. Ten bardzo korzystny przypadek wynika z faktu, że trzy tabele dodane do bazy danych są niezależne od istniejących tabel. Można je było nawet umieścić w oddzielnej bazie danych. W innych przypadkach dodane tabele mogą być powiązane z istniejącymi tabelami. Wówczas konieczne jest zrewidowanie kodu istniejącej warstwy [DAO].

20.3. Klient zaprogramowany dla zabezpieczonej usługi internetowej / jSON

Napisaliśmy już klienta dla niezabezpieczonej usługi internetowej / jSON:

Teraz stworzymy klienta zaprogramowanego dla zabezpieczonej usługi internetowej:

  

20.3.1. Warstwa [Client HTTP]

 

Klasa [Client] zapewnia komunikację HTTP z zabezpieczonym serwerem WWW / jSON. Jak właśnie widzieliśmy, w tej komunikacji HTTP klient musi teraz wysłać nagłówek uwierzytelniający, na przykład:

Authorization:Basic YWRtaW46YWRtaW4=

Interfejs [IClient] przyjmuje następujący wygląd:


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);
}
  • wiersz 8: pierwszym parametrem metody [getResponse] jest teraz obiekt [Credentials], który zawiera identyfikatory użytkownika:

package spring.security.client.entities;

public class Credentials {

    // właściwości
    private String login;
    private String password;

    // konstruktor
    public Credentials() {
    }

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

    // metody pobierające i ustawiające
...
}

Klasa [Client], która implementuje interfejs [IClient], zmienia się w następujący sposób:


package spring.security.client.dao;

...

@Component
public class Client implements IClient {

    // wstrzyknięcia
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // zmienna lokalna
    private String simpleClassName = getClass().getSimpleName();

    private String getBase64(Credentials credentials) {
        // kodowanie użytkownika i hasła w base64 – wymaga 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())));
    }

    // ogólne żądanie
    @Override
    public <T1, T2> T1 getResponse(Credentials credentials, String url, HttpMethod method, int errStatus, T2 body) {
        // odpowiedź serwera
        ResponseEntity<Response<T1>> response;
        try {
            // przygotowuje się żądanie
            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);
            }
            // wykonywanie żądania
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // kapsułkowanie wyjątku
            throw new DaoException(errStatus, e, simpleClassName);
        }
...
    }
...
}
  • wiersze 33–35, 40–42: jeśli użytkownik [credentials] nie jest null, dodawany jest nagłówek uwierzytelniający. Szyfrowanie Base64 użytkownika i jego hasła odbywa się za pomocą metody [getBase64] w wierszach 17–21. Należy zwrócić uwagę na fakt, że metoda ta wykorzystuje klasę [Base64] należącą do JDK 1.8. Nasz klient HTTP może współpracować z niezabezpieczoną usługą internetową. Wystarczy przekazać mu [credentials] równy null;
  • poza powyższymi wierszami kod pozostaje bez zmian;

20.3.2. Warstwa [DAO]

20.3.2.1. Interfejs [IDao]

  

Wszystkie metody interfejsu [IDao] w projekcie [spring-webjson-client-generic] otrzymują dodatkowy parametr [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 {

    // lista wszystkich encji T
    public List<T> getAllShortEntities(Credentials credentials);

    public List<T> getAllLongEntities(Credentials credentials);

    // poszczególnych encji – wersja skrócona
    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);

    // poszczególnych jednostek – wersja długa
    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);

    // aktualizacja wielu elementów
    public List<T> saveEntities(Credentials credentials, Iterable<T> entities);

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

    // usunięcie wszystkich elementów
    public void deleteAllEntities(Credentials credentials);

    // usunięcie wielu elementów
    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);
}
  • wiersz 8: interfejs [IDao] rozszerza następujący interfejs [IAuthenticate]:

package spring.security.client.dao;

import spring.security.client.entities.Credentials;

public interface IAuthenticate {
    // uwierzytelnianie
    public void authenticate(Credentials credentials);
}

Interfejs [IAuthenticate] posiada tylko jedną metodę [authenticate]. Metoda ta nie zwraca żadnej wartości (void), jeśli użytkownik [Credentials credentials] zostanie zaakceptowany przez zabezpieczoną usługę internetową; w przeciwnym razie zgłasza wyjątek.

20.3.2.2. Klasa [AbstractDao]

  

Przypominamy, że klasa [AbstractDao] jest klasą nadrzędną dla klas [DaoCategorie], które zarządzają URL kategorii, oraz [DaoProduit], która zarządza URL produktów. Wszystkie metody klasy [AbstractDao] w projekcie [spring-webjson-client-generic] otrzymują dodatkowy parametr [Credentials credentials], który przekazują do klasy potomnej. Oto przykład:


    @Override
    public List<T1> getShortEntitiesById(Credentials credentials, Iterable<Long> ids) {
        // poprawność argumentu
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // wynik
        return getShortEntitiesById(credentials, Lists.newArrayList(ids));
}
  • metoda [getShortEntitiesById] otrzymuje parametr [Credentials credentials] (wiersz 2), który przekazuje (wiersz 9) do metody [getShortEntitiesById] klasy potomnej;

Klasa [AbstractDao] ma następujący szkielet:


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;

...
}
  • wiersz 14: klasa implementuje interfejs [IDao], który opisaliśmy;
  • wiersze 16–17: wstrzykiwana jest instancja interfejsu [IAuthenticate]. Jest ona implementowana przez następującą klasę [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;

    // weryfikacja [credentials,mdp]
    public void authenticate(Credentials credentials) {
        client.<Void, Void> getResponse(credentials, "/authenticate", HttpMethod.GET, 111, (Void) null);
    }

}
  • wiersz 9: klasa [Authenticate] jest komponentem Spring;
  • wiersz 10: który implementuje interfejs [IAuthenticate];
  • wiersze 11–12: wstrzyknięcie klienta HTTP, który umożliwia komunikację z zabezpieczoną usługą internetową;
  • wiersze 15–17: implementacja metody [authenticate] z interfejsu;
  • wiersz 16: wysyłane jest polecenie HTTP GET do URL [/authenticate]. Zastosowanie tego URL zostało przedstawione w paragrafie 20.2.6. Zasada działania polega na tym, że wywołanie kończy się wyjątkiem, jeśli użytkownik [credentials] jest nieznany lub nie posiada wystarczających uprawnień;

Klasa [AbstractDao] implementuje metodę [authenticate] interfejsu [IDao] w następujący sposób:


    @Autowired
    private IAuthenticate authenticate;

    @Override
    public void authenticate(Credentials credentials) {
        authenticate.authenticate(credentials);
}
  • wiersz 7: zadanie jest przekazywane do metody [authenticate] klasy [Authenticate]. W związku z tym wystąpi wyjątek, jeśli użytkownik [Credentials credentials] nie zostanie zaakceptowany przez zabezpieczoną usługę internetową;

20.3.2.3. Klasy [DaoCategorie, DaoProduit]

  

Klasy [DaoCategorie, DaoProduit] to klasy projektu [spring-webjson-server-generic] z dodatkowym parametrem [Credentials credentials]. Oto przykład:


@Component
public class DaoCategorie extends AbstractDao<Categorie> {

    // zastrzyki
    @Autowired
    protected ApplicationContext context;
    @Autowired
    protected IClient client;

    @Override
    public List<Categorie> getAllShortEntities(Credentials credentials) {
        try {
            // filtry jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // pobierz wszystkie kategorie
            Object map = client.<List<Categorie>, Void> getResponse(credentials, "/getAllShortCategories", HttpMethod.GET,
                    202, null);
            // lista kategorii List<Kategoria>
            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. Konfiguracja Spring

  

Klasa [AppConfig] konfiguruje środowisko Spring projektu. Jest identyczna jak w projekcie [spring-webjson-client-generic], z jednym wyjątkiem:


@Configuration
@ComponentScan({ "spring.security.client.dao" })
public class AppConfig {
  • wiersz 2: należy podać pakiet nowej warstwy [DAO];

20.3.4. Testy warstwy [DAO]

  

20.3.4.1. Test [JUnitTestCredentials]

Test [JUnitTestCredentials] wykorzystuje metodę [IDao.authenticate] do sprawdzenia, czy niektórzy użytkownicy są prawidłowi:


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 {

    // warstwa [DAO]
    @Autowired
    private IAuthenticate authenticate;

    // użytkownicy
    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);
    }
}
  • podczas inicjalizacji klasy testowej, w wierszach 29–34, tworzonych jest trzech użytkowników:
    • użytkownik [admin] ma dostęp do URL serwisu internetowego. Jest on testowany w wierszach 63–72;
    • użytkownik [user] istnieje, ale nie ma uprawnień do korzystania z URL serwisu internetowego. Jest on testowany w wierszach 37–47;
    • użytkownik [unknown] nie istnieje. Jest to sprawdzane w wierszach 50–60;

Uruchamiamy zabezpieczoną usługę internetową z konfiguracją wykonania o nazwie [spring-security-server-jdbc-generic] [1]:

Następnie uruchamia się test JUnit [JUnitTestCredentials] z konfiguracją wykonania [spring-security-client-generic-JUnitTestCredentials] [2]. Uzyskane wyniki w konsoli są następujące:

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"}]

i test zakończył się powodzeniem:

  

20.3.4.2. Test [JUnitTestDao]

Test [JUnitTestDao] jest identyczny jak w niezabezpieczonym projekcie [spring-webjson-client-generic], z tą różnicą,że teraz wszystkie testowane metody warstwy [DAO] mają jako pierwszy parametr użytkownika [admin / admin]:


@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {

    // kontekst Spring
    @Autowired
    private ApplicationContext context;
    // warstwa [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

....

    // użytkownicy
    static private Credentials admin;

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

    @Before
    public void clean() {
        // przed każdym testem czyści się bazę danych
        log("Vidage de la base de données", 1);
        // opróżniamy tabelę [CATEGORIES], a następnie kaskadowo tabelę [PRODUITS]
        daoCategorie.deleteAllEntities(admin);
        // opróżnia się słowniki
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }

    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // wypełnia się tabele
        ...
        // dodajemy kategorię – w konsekwencji produkty również zostaną
        // dodawane – wynik jest zwracany jednocześnie
        return daoCategorie.saveEntities(admin, categories);
    }

    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // lista kategorii
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities(admin);
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // lista produktów
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities(admin);
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // wynik
        return new Object[] { categories, produits };
    }
...

Wszystkie operacje są wykonywane przy użyciu użytkownika [admin / admin], który jako jedyny ma uprawnienia dostępu do zabezpieczonej usługi internetowej.

Test zostanie uruchomiony przy użyciu konfiguracji wykonawczej o nazwie [spring-security-client-generic-JUnitTestDao]:

 

Test zakończył się powodzeniem, ale można zauważyć, że przebiega wolniej niż w przypadku niezabezpieczonej usługi internetowej. Zabezpieczenie aplikacji znacznie wydłuża jej czas odpowiedzi. Należy zwrócić uwagę na istotny czynnik wpływający na wydajność zabezpieczonej usługi internetowej: w klasie [AppConfig], która ją konfiguruje, zapisaliśmy:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // czy aplikacja jest bezpieczna?
        if (activateSecurity) {
            // hasło jest przekazywane w nagłówku Authorization: Basic xxxx
            http.httpBasic();
            // metoda HTTP OPTIONS musi być autoryzowana dla wszystkich
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // tylko rola ADMIN może korzystać z aplikacji
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // wszystkie URL
                    .hasRole("ADMIN");
            // brak sesji
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

Wiersz 17 ma swoje konsekwencje. Decyduje on o tym, czy użytkownik musi uwierzytelnić się przy każdym dostępie. Jeśli go skomentujemy, czas trwania testu JUnit jest znacznie krótszy, ponieważ użytkownik [admin] uwierzytelnia się tylko podczas pierwszego testu, a nie podczas kolejnych (nawet jeśli nagłówek uwierzytelniający HTTP jest wysyłany przez klienta, serwer nie weryfikuje ponownie hasła użytkownika).

20.4. Projekt Eclipse [spring-security-server-jpa-generic]

Zabezpieczona usługa internetowa zostanie teraz zaimplementowana w ramach projektu [spring-security-server-jpa-generic], który opiera się na projekcie [spring-jpa-generic], zarządzającym dostępem do bazy danych za pomocą Spring Data JPA:

Powyżej:

  • warstwa [DAO1] to warstwa [DAO], która zarządza tabelami [PRODUITS] i [CATEGORIES] w bazie [dbproduitscategories]. Została już napisana;
  • warstwa [DAO2] to warstwa [DAO], która zarządza tabelami [USERS], [ROLES] oraz [USERS_ROLES] z bazy [dbproduitscategories]. Pozostaje ją jeszcze utworzyć;

Projekt [spring-security-server-jpa-generic] uzyskuje się najpierw poprzez skopiowanie wcześniej analizowanego projektu [spring-security-server-jdbc-generic]. W rzeczywistości warstwy [web] i [security] nie ulegają zmianie, ponieważ:

  • warstwa [DAO1 / Repositories / JPA] (już napisana) ma ten sam interfejs co warstwa [DAO1 / JDBC];
  • warstwa [DAO2 / Repositories / JPA] (do zapisania) będzie miała ten sam interfejs co warstwa [DAO2 / JDBC];

Projekt [spring-security-server-jpa-generic] wygląda następująco:

  
  • pakiet [spring.security.repositories] implementuje warstwę [repositories];
  • pakiet [spring.security.dao] implementuje warstwę [dao2];

20.4.1. Projekt Maven

Projekt jest projektem Maven skonfigurowanym przez następujący plik [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>
        <!-- serwer WWW / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- wtyczki -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • wiersze 24–27: zależność od warstwy [security] projektu;
  • wiersze 29–33: zależność od warstwy [web] projektu. Projekt [spring-webjson-server-jpa-generic] w pełni implementuje warstwę [web]. Nie ma potrzeby jej pisania ani modyfikowania;

Ostatecznie zależności są następujące:

  

20.4.2. Konfiguracja Spring

  

Plik konfiguracyjny [AppConfig] z poprzedniego projektu [spring-security-server-jdbc-generic] jest odpowiedni. Wystarczy dodać do niego dodatkową konfigurację:


@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 {
  • wiersz 3: deklarujemy pakiet, który implementuje warstwę [repositories];
  • wiersz 4: pakiety zawierające komponenty Spring noszą tę samą nazwę w nowym projekcie;
  • wiersz 5: w poprzednim projekcie klasa [spring.webjson.server.config.AppConfig] znajdowała się w zależności [spring-webjson-server-jdbc-generic]. Tutaj będzie się znajdować w zależności [spring-webjson-server-jpa-generic];

20.4.3. Warstwa JPA

Elementy JPA zarządzane przez warstwę [JPA] znajdują się w projekcie [mysql-config-jpa-hibernate] [2], który jest zależnością projektu [1]:

Klasa [User] jest obrazem tabeli [USERS]:

Image

  • ID: klucz główny;
  • VERSION: kolumna wersji wiersza;
  • IDENTITY: identyfikator opisowy użytkownika;
  • LOGIN: nazwa użytkownika;
  • PASSWORD: hasło użytkownika;

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 {
    // właściwości
    @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;

    // właściwości
    @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;

    // powiązane UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;

    // konstruktorzy
    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;
    }

    // ------------------------------------------------------------
    // przedefiniowanie [equals] i [hashcode]
...

    // metody pobierające i ustawiające
...
}
  • wiersz 23: klasa implementuje interfejs [AbstractCoreEntity], który został już wykorzystany dla innych encji;
  • wiersze 34–35: typ encji. Ta właściwość nie jest zapisywana w bazie danych [@Transient];
  • wiersze 38–43: trzy podstawowe właściwości użytkownika (name, login, password);
  • wiersze 46–48: lista ról użytkownika. Użytkownik może pełnić wiele ról. Podobnie, jak zobaczymy, z jedną rolą może być powiązanych wielu użytkowników. Mamy zatem, w rozumieniu terminu JPA, relację [ManyToMany] między encjami [User] i [Role]:
    • użytkownik może odnosić się do wielu ról;
    • jedna rola może odnosić się do wielu użytkowników;

Ta relacja [ManyToMany] jest zaimplementowana w bazie danych za pomocą tabeli połączeniowej [USERS_ROLES]. Jeśli użytkownik U jest powiązany z rolą R, w tabeli [USERS_ROLES] zapisuje się tę relację poprzez zarejestrowanie pary kluczy głównych tych encji (U, R). W tabeli JPA relacja [ManyToMany] łącząca encje [User] i [Role] może zostać podzielona na dwie relacje [ManyToOne, OneToMany]:

  • (ciąg dalszy)
    • relację [ManyToOne] z encji [User] do encji [UserRole];
    • relację [OneToMany] z encji [UserRole] do encji [UserRole];

Podobnie relacja [ManyToMany] łącząca encje [Role] i [User] może zostać podzielona na dwie relacje [ManyToOne, OneToMany]:

  • (ciąg dalszy)
    • relację [ManyToOne] z encji [Role] do encji [UserRole];
    • relację [OneToMany] z encji [UserRole] do encji [User];
  • związek 48: fakt, że użytkownik pełni wiele ról, jest odzwierciedlony przez relację [OneToMany] do encji [UserRole];

Klasa [Role] jest odwzorowaniem tabeli [ROLES]:

Image

  • ID: klucz główny;
  • VERSION: kolumna wersjonowania wiersza;
  • NAME: nazwa roli. Domyślnie Spring Security oczekuje nazw w postaci ROLE_XX, na przykład ROLE_ADMIN lub 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 {
    // właściwości
    @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;

    // właściwości
    @Column(name = ConfigJdbc.TAB_ROLES_NAME, length = 30, unique = true, nullable = false)
    private String name;

    // powiązane UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "role", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;

    // konstruktory
    public Role() {
    }

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

    // metody pobierające i ustawiające
    public Role(String name) {
        this.name = name;
    }

    // ------------------------------------------------------------
    // przedefiniowanie [equals] i [hashcode]
...
    // metody pobierające i ustawiające
...
}
  • wiersze 42–44: fakt, że z jedną rolą może być powiązanych wielu użytkowników, jest odzwierciedlony przez relację [@OneToMany] do encji [UserRole];

Klasa [UserRole] jest odwzorowaniem tabeli [USERS_ROLES]:

Image

Użytkownik może pełnić wiele ról, a jedna rola może obejmować wielu użytkowników. Mamy tu do czynienia z relacją wiele-do-wielu, którą odzwierciedla tabela [USERS_ROLES].

  • ID: klucz główny;
  • VERSION: kolumna wersji wiersza;
  • USER_ID: identyfikator użytkownika;
  • ROLE_ID: identyfikator roli;

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 {
    // właściwości
    @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 odwołuje się do obiektu typu User
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_USER_ID, nullable = false)
    private User user;

    // UserRole odwołuje się do roli
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_ROLE_ID, nullable = false)
    private Role role;

    // konstruktorzy
    public UserRole() {

    }

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

    // ------------------------------------------------------------
    // przedefiniowanie [equals] i [hashcode]
    ...

    // metody pobierające i ustawiające
...
}
  • wiersze 34–36: definiują klucz obcy z tabeli [USERS_ROLES] do tabeli [USERS];
  • wiersze 38–41: definiują klucz obcy z tabeli [USERS_ROLES] do tabeli [ROLES];

20.4.4. Warstwa [repositories]

  

Interfejs [UserRepository] zarządza dostępem do encji [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> {

    // lista ról użytkownika zidentyfikowanego na podstawie jego identyfikatora
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lista ról użytkownika zidentyfikowanego na podstawie unikalnego loginu
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // wyszukiwanie użytkownika na podstawie jego loginu
    User findUserByLogin(String login);
}
  • wiersz 9: interfejs [UserRepository] rozszerza interfejs [CrudRepository] z biblioteki Spring Data (wiersz 7);
  • wiersze 12–13: metoda [getRoles(long id)] pozwala uzyskać wszystkie role użytkownika zidentyfikowanego za pomocą jego [id]
  • wiersze 16–17: to samo, ale dla użytkownika zidentyfikowanego na podstawie loginu i hasła;
  • wiersz 20: wyszukiwanie użytkownika na podstawie loginu;

Interfejs [RoleRepository] zarządza dostępem do jednostek [Role]:


package spring.security.repositories;

import generic.jpa.entities.dbproduitscategories.Role;

import org.springframework.data.repository.CrudRepository;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // wyszukiwanie roli na podstawie nazwy
    Role findRoleByName(String name);

}
  • wiersz 7: interfejs [RoleRepository] rozszerza interfejs [CrudRepository];
  • wiersz 10: można wyszukiwać rolę według jej nazwy. Przypominamy, że encja [Role] posiada pole [name]. Metoda [findEntityByChamp] jest automatycznie implementowana przez Spring Data. Nie ma zatem potrzeby implementowania tutaj metody [finRoleByName]. Wystarczy ją zadeklarować w interfejsie.

Interfejs [UserRoleRepository] zarządza dostępem do encji [UserRole]:


package spring.security.repositories;

import generic.jpa.entities.dbproduitscategories.UserRole;

import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • wiersz 7: interfejs [UserRoleRepository] jedynie rozszerza interfejs [CrudRepository], nie dodając do niego nowych metod;

20.4.5. Warstwa [DAO2]

  

W warstwie [DAO2] znajdują się te same klasy, co w warstwie [DAO2] projektu [spring-security-server-jdbc-generic] omówionego wcześniej w paragrafie 20.2.2. Teraz wystarczy je zaimplementować przy pomocy klas z warstwy [repositories].

Klasa [AppUserDetails] zmienia się w następujący sposób:


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;

    // właściwości
    private User user;
    private UserRepository userRepository;
    
    // lokalne
    private String simpleClassName = getClass().getSimpleName();

    // konstruktorzy
    public AppUserDetails() {
    }

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

    // -------------------------interfejs
    @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;
    }
...
}
  • w wierszu 31 konstruktor klasy otrzymuje jako drugi parametr obiekt [UserRepository], który umożliwia klasie uzyskanie ról danego użytkownika (wiersz 42);

Komponent Spring [AppUserDetailsService] zmienia się natomiast w następujący sposób:


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;
    // lokalny
    private String simpleClassName = getClass().getName();

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // szukamy użytkownika na podstawie jego loginu
        User user;
        try {
            user = userRepository.findUserByLogin(login);
        } catch (Exception e) {
            throw new DaoException(168, e, simpleClassName);
        }
        // znaleziono?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // wyświetlanie szczegółów użytkownika
        return new AppUserDetails(user, userRepository);
    }

}
  • wiersz 18: wstrzyknięcie komponentu Spring [userRepository], który umożliwi usłudze zwrócenie użytkownika zidentyfikowanego na podstawie loginu, wiersz 27;

Ostatecznie okazuje się, że potrzebujemy tylko repozytorium [userRepository], a nie dwóch pozostałych repozytoriów [roleRepository, userRoleRepository]. Te ostatnie zostaną wykorzystane w kolejnym projekcie, którego celem jest wypełnienie tabel [USERS, ROLES, USERS_ROLES].

20.4.6. Testy

Zabezpieczona usługa internetowa jest uruchamiana z konfiguracją o nazwie [spring-security-server-jpa-generic-hibernate-eclipselink] [1]. Test [JUnitTestDao] dla klienta ogólnego jest uruchamiany z konfiguracją o nazwie [spring-security-client-generic-JUnitTestDao] [2]:

Testy zakończyły się powodzeniem.

20.5. Projekt Eclipse [spring-security-create-users]

  

20.5.1. Baza danych

Wykonanie projektu wypełnia tabele [USERS, ROLES, USERS_ROLES] z tabeli [dbproduitscategories]:

Image

 

Utworzone identyfikatory [login/passwd] to: [admin/admin], [user/user], [guest/guest]. W bazie hasła są zaszyfrowane.

Image

20.5.2. Konfiguracja Maven

Projekt jest projektem Maven skonfigurowanym za pomocą następującego pliku [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>
  • wiersze 22–25: zależność od frameworka Spring Security. Algorytm szyfrowania haseł jest dostarczany przez ten framework
  • wiersze 27–31: zależność od projektu [spring-security-server-jpa-generic], który właśnie skompilowaliśmy. Projekt ten implementuje warstwy [repositories] i [JPA] projektu;

Ostatecznie zależności są następujące:

  

20.5.3. Warstwa [console]

Ponieważ warstwy [repositories] i [JPA] są zaimplementowane przez zależność [spring-security-server-jpa-generic], pozostaje do zaimplementowania jedynie warstwa [console].

  
  • [AppConfig] to klasa konfiguracyjna Spring dla projektu;
  • [CreateUsers] to klasa wykonawcza, która tworzy użytkowników i role;
  • [Base64Encoder] to klasa pomocnicza służąca do generowania kodu Base64 dla pary [login, password]. Korzystaliśmy z niej już wcześniej. Nie jest ona potrzebna w tym projekcie;

Klasa konfiguracyjna Spring [AppConfig] wygląda następująco:


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 {
}
  • wiersz 10: wskazujemy, gdzie znajdują się pliki [repositories] aplikacji. Znajdują się one w pakiecie [spring.security.repositories] zależności [spring-security-server-jpa-generic]
  • wiersz 11: importuje się bean klasy [ConfigJpa], która konfiguruje warstwę [JPA] projektu. Klasę tę można znaleźć w zależności [mysql-config-jpa-hibernate]:
  

Klasa [CreateUsers] ma następującą postać:


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) {

        // koniec
        System.out.println("Travail en cours...");

        // tworzymy trzech użytkowników
        String[] logins = { "admin", "user", "guest" };
        String[] passwds = { "admin", "user", "guest" };
        String[] roles = { "admin", "user", "guest" };

        // kontekst 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++) {
            // pobieramy informacje o użytkowniku nr i
            String login = logins[i];
            String password = passwds[i];
            String roleName = String.format("ROLE_%s", roles[i].toUpperCase());
            // czy rola już istnieje?
            Role role = roleRepository.findRoleByName(roleName);
            // jeśli nie istnieje, tworzymy ją
            if (role == null) {
                role = roleRepository.save(new Role(roleName));
            }
            // czy użytkownik już istnieje?
            User user = userRepository.findUserByLogin(login);
            // jeśli nie istnieje, tworzymy go
            if (user == null) {
                // haszujemy hasło za pomocą bcrypt
                String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
                // zapisujemy użytkownika
                user = userRepository.save(new User(null, null, login, login, crypt));
                // tworzymy powiązanie z rolą
                userRoleRepository.save(new UserRole(user, role));
            } else {
                // użytkownik już istnieje – czy posiada żądaną rolę?
                boolean trouvé = false;
                for (Role r : userRepository.getRoles(user.getId())) {
                    if (r.getName().equals(roleName)) {
                        trouvé = true;
                        break;
                    }
                }
                // jeśli nie znaleziono, tworzy się powiązanie z rolą
                if (!trouvé) {
                    userRoleRepository.save(new UserRole(user, role));
                }
            }
        }
        // zamknięcie kontekstu Spring
        context.close();
        // koniec
        System.out.println("Travail terminé...");
    }

}
  • wiersze 22–24: definiują nazwę użytkownika, hasło i rolę trzech użytkowników;
  • wiersz 27: kontekst Springa jest tworzony na podstawie klasy konfiguracyjnej [AppConfig];
  • wiersze 28–30: pobierane są odniesienia do trzech obiektów [Repository], które mogą być przydatne do utworzenia użytkownika;
  • wiersz 31: tworzy się trzech użytkowników;
  • wiersze 33–35: informacje potrzebne do utworzenia użytkownika nr i;
  • wiersz 37: sprawdzamy, czy rola już istnieje;
  • wiersze 39–41: jeśli nie, tworzymy ją w bazie danych. Będzie miała nazwę typu [ROLE_XX];
  • wiersz 43: sprawdzamy, czy login już istnieje;
  • wiersze 45–52: jeśli nazwa użytkownika nie istnieje, tworzymy ją w bazie danych;
  • wiersz 47: szyfrujemy hasło. Wykorzystujemy tutaj klasę [BCrypt] z biblioteki Spring Security (wiersz 8). Potrzebujemy więc archiwów tego frameworka. Plik [pom.xml] zawiera następującą zależność:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • wiersz 49: użytkownik jest zapisywany w bazie danych;
  • wiersz 51: podobnie jak relacja łącząca go z jego rolą;
  • wiersze 55–60: sytuacja, w której login już istnieje – sprawdzamy wówczas, czy wśród jego ról znajduje się już ta, którą chcemy mu przypisać;
  • wiersze 62–64: jeśli nie znaleziono poszukiwanej roli, tworzy się wiersz w tabeli [USERS_ROLES], aby powiązać użytkownika z jego rolą;
  • nie zabezpieczono się przed ewentualnymi wyjątkami. Jest to klasa pomocnicza służąca do szybkiego tworzenia użytkowników;

Aby uruchomić projekt, należy uruchomić konfigurację uruchomieniową o nazwie [spring-security-create-users-hibernate-eclipselink]:

 

Właśnie stworzyliśmy dwie bezpieczne usługi internetowe:

  • jedną z architekturą [security / web / JDBC / MySQL];
  • druga oparta na architekturze [security / web / Hibernate / MySQL];

Teraz zajmiemy się dwiema kolejnymi architekturami:

  • architekturę [security / web / EclipseLink / SQL Server 2014 Express];
  • architekturę [security / web / OpenJpa / Oracle Express];

  • w [1] ładuje się projekty konfigurujące warstwę [JDBC / SQL Server] oraz warstwę [JPA / EclipseLink / SQL Server];

Uwaga: naciśnij klawisze Alt-F5, a następnie zregeneruj wszystkie projekty Maven.

Zakładamy, że serwer SGBD SQL został uruchomiony, a baza danych [dbproduitscategories] została wygenerowana. Najpierw musimy wypełnić tabele [USERS, ROLES, USERS_ROLES] w tej bazie danych. W tym celu należy uruchomić konfigurację wykonania o nazwie [spring-security-create-users-hibernate-eclipselink]:

Powinna ona wypełnić te trzy tabele danymi:

 
 
  • uruchom zabezpieczoną usługę internetową z konfiguracją o nazwie [spring-security-server-jpa-generic-hibernate-eclipselink][1];
  • uruchom test JUnitTestDao przy użyciu konfiguracji o nazwie [spring-security-client-generic-JUnitTestDao][2]. Test powinien zakończyć się powodzeniem [3];

20.5.5. Architektura [security / web / OpenJpa / Oracle Express]

  • w [1] ładuje się projekty konfigurujące warstwę [JDBC / Oracle Express] oraz warstwę [JPA / OpenJpa / Oracle Express];

Uwaga: należy nacisnąć klawisze Alt-F5, a następnie ponownie wygenerować wszystkie projekty Maven.

Zakładamy, że serwer Oracle Express o nazwie SGBD został uruchomiony, a baza danych [dbproduitscategories] została wygenerowana. Najpierw musimy wypełnić tabele [USERS, ROLES, USERS_ROLES] w tej bazie danych. W tym celu należy uruchomić konfigurację o nazwie [spring-security-create-users-openjpa]:

Powinna ona wypełnić te trzy tabele danymi:

 
 
  • uruchom zabezpieczoną usługę internetową z konfiguracją o nazwie [spring-security-server-jpa-generic-openjpa][1-2];
  • uruchom test JUnitTestDao przy użyciu konfiguracji o nazwie [spring-security-client-generic-JUnitTestDao][3]. Test powinien zakończyć się powodzeniem [4];