Skip to content

16. [Cours]: Proteggere l'accesso a un servizio web con Spring Security

Parole chiave: architettura a più livelli, Spring, iniezione di dipendenze, servizio web / jSON protetto, client / server

16.1. Support

 

I progetti di questo capitolo si trovano nella cartella [support / chap-16]. Lo script SQL serve a generare il database necessario per i test.

16.2. Il ruolo di Spring Security in un'applicazione web

Inquadriamo Spring Security nello sviluppo di un’applicazione web. Nella maggior parte dei casi, questa sarà costruita su un’architettura a più livelli come la seguente:

  • il livello [Spring Security] concede l’accesso al livello [web] solo agli utenti autorizzati.

16.3. Un tutorial su Spring Security

Importiamo nuovamente una guida Spring seguendo i passaggi da 1 a 3 riportati di seguito:

  

Il progetto è composto dai seguenti elementi:

  • nella cartella [templates] si trovano le pagine HTML del progetto;
  • [Application]: è la classe eseguibile del progetto;
  • [MvcConfig]: è la classe di configurazione di Spring MVC;
  • [WebSecurityConfig]: è la classe di configurazione di Spring Security;

16.3.1. Configurazione Maven

Il progetto [3] è un progetto Maven. Esaminiamo il suo file [pom.xml] per conoscere le sue dipendenze:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-securing-web</artifactId>
    <version>0.1.0</version>

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::security[] -->
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • righe 10-14: il progetto è un progetto Spring Boot;
  • righe 17-20: dipendenza dal framework [Thymeleaf];
  • righe 22-25: dipendenza dal framework Spring Security;

16.3.2. Le viste Thymeleaf

  

La vista [home.html] è la seguente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>

    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • riga 12: l'attributo [th:href="@{/hello}"] genererà l'attributo [href] del tag [<a>]. Il valore [@{/hello}] genererà il percorso [<context>/hello], dove [context] è il contesto dell'applicazione web;

Il codice HTML generato è il seguente:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

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

La vista [hello.html] è la seguente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • riga 9: l'attributo [th:inline="text"] genererà il testo del tag [<h1>]. Questo testo contiene un'espressione $ che deve essere valutata. L'elemento [[${#httpServletRequest.remoteUser}]] è il valore dell'attributo [RemoteUser] della richiesta HTTP corrente. Si tratta del nome dell'utente connesso;
  • riga 10: un modulo HTML. L'attributo [th:action="@{/logout}"] genererà l'attributo [action] del tag [form]. Il valore [@{/logout}] genererà il percorso [<context>/logout], dove [context] è il contesto dell'applicazione web;

Il codice HTML generato è il seguente:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello user!</h1>
        <form method="post" action="/logout">
            <input type="submit" value="Sign Out" />
            <input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
        </form>
    </body>
</html>
  • riga 8: la traduzione di Hello [[${#httpServletRequest.remoteUser}]]!;
  • riga 9: la traduzione di @{/logout};
  • riga 11: un campo nascosto denominato (attributo name) _csrf;

La vista [login.html] è la seguente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • riga 9: l'attributo [th:if="${param.error}"] fa sì che il tag <div> venga generato solo se l'URL che visualizza la pagina di login contiene il parametro [error] (http://context/login?error);
  • riga 10: l'attributo [th:if="${param.logout}"] fa sì che il tag <div> venga generato solo se l'URL che visualizza la pagina di login contiene il parametro [logout] (http://context/login?logout);
  • righe 11-23: un modulo HTML;
  • riga 11: il modulo verrà inviato a URL [<context>/login], dove <context> è il contesto dell'applicazione web;
  • riga 13: un campo di immissione denominato [username];
  • riga 17: un campo di immissione denominato [password];

Il codice HTML generato è il seguente:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>

        <div>
            You have been logged out.
        </div>
        <form method="post" action="/login">
            <div>
                <label>
                    User Name :
                    <input type="text" name="username" />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" name="password" />
                </label>
            </div>
            <div>
                <input type="submit" value="Sign In" />
            </div>
            <input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

Si noti che alla riga 28 Thymeleaf ha aggiunto un campo nascosto denominato [_csrf].

16.3.3. Configurazione Spring MVC

  

La classe [MvcConfig] configura il framework Spring MVC:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}
  • riga 7: l'annotazione [@Configuration] rende la classe [MvcConfig] una classe di configurazione;
  • riga 8: la classe [MvcConfig] estende la classe [WebMvcConfigurerAdapter] per ridefinirne alcuni metodi;
  • riga 10: ridefinizione di un metodo della classe padre;
  • righe 11-16: il metodo [addViewControllers] consente di associare URL a viste HTML. Vengono effettuate le seguenti associazioni:
URL
vista
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

Il suffisso [html] e la cartella [templates] sono i valori predefiniti utilizzati da Thymeleaf. Possono essere modificati tramite configurazione. La cartella [templates] deve trovarsi nella radice del Classpath del progetto:

Sopra [1], le cartelle [java] e [resources] sono entrambe cartelle sorgente (source folders). Ciò implica che il loro contenuto si troverà nella radice del Classpath del progetto. Pertanto, in [2], le cartelle [hello] e [templates] si troveranno alla radice del Classpath.

16.3.4. Configurazione di Spring Security

  

La classe [WebSecurityConfig] configura il framework Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • riga 9: l'annotazione [@Configuration] rende la classe [WebSecurityConfig] una classe di configurazione;
  • riga 10: l'annotazione [@EnableWebSecurity] trasforma la classe [WebSecurityConfig] in una classe di configurazione di Spring Security;
  • riga 11: la classe [WebSecurity] estende la classe [WebSecurityConfigurerAdapter] per ridefinirne alcuni metodi;
  • riga 12: ridefinizione di un metodo della classe padre;
  • righe 13-16: il metodo [configure(HttpSecurity http)] viene ridefinito per definire i diritti di accesso alle diverse URL dell’applicazione;
  • riga 14: il metodo [http.authorizeRequests()] consente di associare i URL ai diritti di accesso. Vengono effettuate le seguenti associazioni:
URL
regola
codice
/, /home
accesso senza autenticazione

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
accesso solo con autenticazione
http.anyRequest().authenticated();
  • riga 15: definisce il metodo di autenticazione. L'autenticazione avviene tramite un modulo accessibile a tutti. La disconnessione (logout) è anch'essa accessibile a tutti;
  • righe 19-21: ridefiniscono il metodo [configure(AuthenticationManagerBuilder auth)] che gestisce gli utenti;
  • riga 20: l’autenticazione avviene tramite utenti definiti in modo “fisso” [auth.inMemoryAuthentication()]. Un utente è qui definito con il login [user], la password [password] e il ruolo [USER]. È possibile assegnare gli stessi diritti agli utenti che hanno lo stesso ruolo;

16.3.5. Classe eseguibile

  

La classe [Application] è la seguente:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }

}
  • riga 8: l'annotazione [@EnableAutoConfiguration] richiede a Spring Boot (riga 3) di effettuare la configurazione che lo sviluppatore non avrà effettuato esplicitamente;
  • riga 9: rende la classe [Application] una classe di configurazione Spring;
  • riga 10: richiede la scansione della cartella della classe [Application] per individuare i componenti Spring. Le due classi [MvcConfig] e [WebSecurityConfig] verranno così individuate poiché presentano l'annotazione [@Configuration];
  • riga 13: il metodo [main] della classe eseguibile;
  • riga 14: il metodo statico [SpringApplication.run] viene eseguito con come parametro la classe di configurazione [Application]. Abbiamo già incontrato questo processo e sappiamo che il server Tomcat incluso nelle dipendenze Maven del progetto verrà avviato e il progetto verrà distribuito su di esso. Abbiamo visto che quattro URL erano gestiti da [/, /home, /login, /hello] e che alcuni erano protetti da diritti di accesso.

16.3.6. Test dell’applicazione

Iniziamo richiedendo il URL [/], che è uno dei quattro URL accettati. È associato alla vista [/templates/home.html]:

 

La URL richiesta, [/], è accessibile a tutti. Ecco perché l’abbiamo ottenuta. Il link [here] è il seguente:

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

Il URL [/hello] verrà richiesto quando si cliccherà sul link. Questo è protetto:

URL
regola
codice
/, /home
accesso senza autenticazione

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
accesso riservato agli utenti autenticati
http.anyRequest().authenticated();

È necessario effettuare l'autenticazione per ottenerlo. Spring Security reindirizzerà quindi il browser del cliente alla pagina di autenticazione. In base alla configurazione visualizzata, si tratta della pagina URL [/login]. Questa è accessibile a tutti:


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

Otteniamo quindi [1]:

Il codice sorgente della pagina ottenuta è il seguente:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • alla riga 7 compare un campo nascosto che non è presente nella pagina originale [login.html]. È stato aggiunto da Thymeleaf. Questo codice, denominato CSRF (Cross Site Request Forgery), ha lo scopo di eliminare una vulnerabilità di sicurezza. Questo token deve essere rinviato a Spring Security insieme all’autenticazione affinché quest’ultima venga accettata;

Ricordiamo che solo l’utente user/password viene riconosciuto da Spring Security. Se inseriamo qualcos’altro in [2], otteniamo la stessa pagina con un messaggio di errore in [3]. Spring Security ha reindirizzato il browser alla pagina URL [http://localhost:8080/login?error]. La presenza del parametro [error] ha attivato la visualizzazione del tag:


<div th:if="${param.error}">Invalid username and password.</div>

Ora inseriamo i valori previsti per user/password [4]:

  • in [4], effettuiamo l’autenticazione;
  • in [5], Spring Security ci reindirizza verso URL [/hello] poiché è URL che avevamo richiesto quando siamo stati reindirizzati alla pagina di login. L’identità dell’utente è stata visualizzata dalla seguente riga di [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

La pagina [5] mostra il seguente modulo:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

Quando si fa clic sul pulsante [Sign Out], verrà eseguito un POST su URL [/logout]. Quest’ultimo, così come il URL e il [/login], è accessibile a tutti:


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

Nella nostra associazione URL / viste, non abbiamo definito nulla per URL e [/logout]. Cosa succederà? Proviamo:

  • in [6], clicchiamo sul pulsante [Sign Out];
  • da [7], vediamo che siamo stati reindirizzati a URL [http://localhost:8080/login?logout]. È stato Spring Security a richiedere questo reindirizzamento. La presenza del parametro [logout] in URL ha fatto visualizzare la seguente riga nella vista:

<div th:if="${param.logout}">You have been logged out.</div>

16.3.7. Conclusione

Nell’esempio precedente, avremmo potuto scrivere prima l’applicazione web e poi renderla sicura. Spring Security non è invasivo. È possibile implementare la sicurezza di un’applicazione web già scritta. Inoltre, abbiamo scoperto quanto segue:

  • è possibile definire una pagina di autenticazione;
  • l’autenticazione deve essere accompagnata dal token CSRF emesso da Spring Security;
  • se l’autenticazione fallisce, si viene reindirizzati alla pagina di autenticazione con l’aggiunta di un parametro «error» nel token URL;
  • se l’autenticazione va a buon fine, si viene reindirizzati alla pagina richiesta al momento dell’autenticazione. Se si richiede direttamente la pagina di autenticazione senza passare da una pagina intermedia, Spring Security reindirizza verso il URL [/] (questo caso non è stato presentato);
  • ci si disconnette richiedendo la pagina URL [/logout] con un POST. Spring Security ci reindirizza quindi alla pagina di autenticazione con il parametro logout nel URL;

Tutte queste conclusioni si basano sui comportamenti predefiniti di Spring Security. Tali comportamenti possono essere modificati tramite configurazione, ridefinendo alcuni metodi della classe [WebSecurityConfigurerAdapter].

Il tutorial precedente ci sarà di scarso aiuto nel prosieguo. Utilizzeremo infatti:

  • un database per memorizzare gli utenti, le loro password e i loro ruoli;
  • un'autenticazione tramite header HTTP;

Esistono pochi tutorial relativi a ciò che vogliamo realizzare in questa sede. La soluzione che verrà proposta è un insieme di codici trovati qua e là.

16.4. Implementazione della sicurezza sul servizio web / json dei prodotti

16.4.1. Il database

Il database [dbintrospringdata] viene aggiornato per includere gli utenti, le loro password e i loro ruoli. Vengono aggiunte tre nuove tabelle:

Image

Tabella [USERS]: gli utenti

  • ID: chiave primaria;
  • VERSION: colonna di versioning della riga;
  • IDENTITY: un'identità descrittiva dell'utente;
  • LOGIN: il nome utente;
  • PASSWORD: la sua password;

Nella tabella USERS, le password non sono memorizzate in chiaro:

 

L'algoritmo che crittografa le password è l'algoritmo BCRYPT.

Tabella [ROLES]: i ruoli

  • ID: chiave primaria;
  • VERSION: colonna di versioning della riga;
  • NAME: nome del ruolo. Per impostazione predefinita, Spring Security si aspetta nomi del tipo ROLE_XX, ad esempio ROLE_ADMIN o ROLE_GUEST;
 

Tabella [USERS_ROLES]: tabella di join USERS / ROLES

Un utente può ricoprire più ruoli, mentre un ruolo può comprendere più utenti. Si tratta di una relazione molti-a-molti rappresentata dalla tabella [USERS_ROLES].

  • ID: chiave primaria;
  • VERSION: colonna di versioning della riga;
  • USER_ID: identificativo di un utente;
  • ROLE_ID: identificativo di un ruolo;
 

16.4.2. Il progetto Eclipse

Creiamo il seguente progetto Eclipse:

1
  

  • in [1]: il nuovo progetto con i seguenti pacchetti:
    • [spring.security.entities]: contiene le entità JPA corrispondenti alle tre nuove tabelle del database;
    • [spring.security.repositories]: contiene i [repositories] di Spring Data associati alle tre nuove tabelle;
    • [spring.security.dao]: contiene un servizio basato sui [repositories];
    • [spring.security.config]: contiene la configurazione del progetto, in particolare quella relativa agli accessi protetti al servizio web;
    • [spring.security.boot]: contiene la classe di avvio del servizio web protetto;

16.4.3. La configurazione Maven

Il nuovo progetto è un progetto Maven configurato dal seguente file [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring.security</groupId>
    <artifactId>intro-spring-security-server-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

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

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

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

    <dependencies>
        <dependency>
            <groupId>istia.st.webjson</groupId>
            <artifactId>intro-server-webjson-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Log di Spring -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Test di Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- plugin -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • righe 23-27: si riprende l’esistente con l’archivio del servizio web /json analizzato;
  • righe 29-32: la dipendenza che introduce le classi di Spring Security;
  • righe 34-37: la libreria di log;
  • righe 39-42: la libreria che consente di utilizzare le annotazioni di Spring Boot;
  • righe 44-48: la libreria necessaria per i test;

16.4.4. Le nuove entità [JPA]

Il livello JPA definisce tre nuove entità:

  

La classe [User] è l'immagine della tabella [USERS]:


package spring.security.entities;

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

import spring.data.entities.AbstractEntity;

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

    // proprietà
    @Column(name = "NAME")
    private String name;
    @Column(name = "LOGIN")
    private String login;
    @Column(name = "PASSWORD")
    private String password;

    // Costruttore
    public User() {
    }

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

    // getter e setter
...
}
  • riga 11: la classe estende la classe [AbstractEntity] già utilizzata per le altre entità;

La classe [Role] è l'immagine della tabella [ROLES]:


package spring.security.entities;

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

import spring.data.entities.AbstractEntity;

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

    // proprietà
    @Column(name="NAME")
    private String name;

    // costruttori
    public Role() {
    }

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

    // getter e setter
    public String getName() {
        return name;
    }

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

}

La classe [UserRole] è l'immagine della tabella [USERS_ROLES]:


package spring.security.entities;

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

import spring.data.entities.AbstractEntity;

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

    // chiavi esterne
    @Column(name = "USER_ID", insertable = false, updatable = false)
    private Long userId;
    @Column(name = "ROLE_ID", insertable = false, updatable = false)
    private Long roleId;

    // un UserRole fa riferimento a un utente
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;

    // un UserRole fa riferimento a un ruolo
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // costruttori
    public UserRole() {

    }

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

    // getter e setter
...
}

  • righe 22-24: definiscono la chiave esterna dalla tabella [USERS_ROLES] alla tabella [USERS];
  • righe 27-29: definiscono la chiave esterna dalla tabella [USERS_ROLES] alla tabella [ROLES];

16.4.5. Le tabelle [repositories]

Ciascuna delle precedenti entità JPA è gestita da un [repository] Spring Data:

  

L'interfaccia [UserRepository] gestisce l'accesso alle entità [User]:


package spring.security.repositories;

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

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

public interface UserRepository extends CrudRepository<User, Long> {

    // elenco dei ruoli di un utente identificato dal proprio ID
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // elenco dei ruoli di un utente identificato dal proprio login univoco
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // ricerca di un utente tramite il suo login
    User findUserByLogin(String login);
}
  • riga 9: l'interfaccia [UserRepository] estende l'interfaccia [CrudRepository] di Spring Data (riga 4);
  • righe 12-13: il metodo [getRoles(User user)] consente di ottenere tutti i ruoli di un utente identificato tramite il suo [id]
  • righe 16-17: lo stesso, ma per un utente identificato tramite login e password;
  • riga 20: per trovare un utente tramite il suo nome utente;

L'interfaccia [RoleRepository] gestisce gli accessi alle entità [Role]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // ricerca di un ruolo tramite il suo nome
    Role findRoleByName(String name);

}
  • riga 7: l'interfaccia [RoleRepository] estende l'interfaccia [CrudRepository];
  • riga 10: è possibile cercare un ruolo tramite il suo nome;

L'interfaccia [UserRoleRepository] gestisce gli accessi alle entità [UserRole]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.UserRole;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • riga 5: l'interfaccia [UserRoleRepository] si limita a estendere l'interfaccia [CrudRepository] senza aggiungere nuovi metodi;

16.4.6. Le classi di gestione degli utenti e dei ruoli

  

Spring Security richiede la creazione di una classe che implementi la seguente interfaccia [UsersDetail]:

 

Tale interfaccia è qui implementata dalla classe [AppUserDetails]:


package spring.security.dao;

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

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

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

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // proprietà
    private User user;
    private UserRepository userRepository;

    // costruttori
    public AppUserDetails() {
    }

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

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

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

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

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

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

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

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

    // getter e setter
    ...
}
  • riga 14: la classe [AppUserDetails] implementa l'interfaccia [UserDetails];
  • righe 19-20: la classe incapsula un utente (riga 19) e il repository che consente di ottenere i dettagli di tale utente (riga 20);
  • righe 26-29: il costruttore che istanzia la classe con un utente e il relativo repository;
  • righe 32-36: implementazione del metodo [getAuthorities] dell’interfaccia [UserDetails]. Deve costruire una collezione di elementi di tipo [GrantedAuthority] o derivati. In questo caso, utilizziamo il tipo derivato [SimpleGrantedAuthority] (riga 36) che incapsula il nome di uno dei ruoli dell'utente della riga 19;
  • righe 35-37: si scorre l'elenco dei ruoli dell'utente della riga 19 per creare un elenco di elementi di tipo [SimpleGrantedAuthority];
  • righe 42-44: implementano il metodo [getPassword] dell'interfaccia [UserDetails]. Viene restituita la password dell'utente della riga 19;
  • righe 42-44: implementano il metodo [getUserName] dell'interfaccia [UserDetails]. Viene restituito il nome utente della riga 19;
  • righe 51-54: l'account dell'utente non scade mai;
  • righe 56-59: l'account dell'utente non viene mai bloccato;
  • righe 61-64: le credenziali dell'utente non scadono mai;
  • righe 66-69: l'account dell'utente è sempre attivo;

Spring Security richiede inoltre l’esistenza di una classe che implementi l’interfaccia [AppUserDetailsService]:

 

Questa interfaccia è implementata dalla seguente classe [AppUserDetailsService]:


package spring.security.dao;

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

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

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // ricerca dell'utente tramite login
        User user = userRepository.findUserByLogin(login);
        // Trovato?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // vengono restituiti i dettagli dell'utente
        return new AppUserDetails(user, userRepository);
    }

}
  • riga 12: la classe sarà un componente Spring, quindi disponibile nel proprio contesto;
  • righe 15-16: il componente [UserRepository] verrà iniettato qui;
  • righe 19-28: implementazione del metodo [loadUserByUsername] dell’interfaccia [UserDetailsService] (riga 10). Il parametro è il login dell’utente;
  • riga 21: l'utente viene cercato tramite il suo nome utente;
  • righe 23-25: se non viene trovato, viene generata un'eccezione;
  • riga 27: viene creato e restituito un oggetto [AppUserDetails]. È effettivamente di tipo [UserDetails] (riga 19);

16.4.7. La configurazione del progetto

Il progetto è configurato da due classi:

La classe [DaoConfig] configura il livello [DAO] introdotto dal nuovo progetto:


package spring.security.config;

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

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

    // costanti
    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };

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

}
  • riga 10: si importa la classe di configurazione [spring.data.config.DaoConfig] dal progetto [intro-spring-data-01] che implementa il livello [DAO] relativo a prodotti e categorie;
  • riga 8: si indicano le cartelle del progetto corrente contenenti i componenti Spring Data [repositories];
  • riga 9: si indicano le cartelle del progetto corrente contenenti i componenti Spring relativi al livello [DAO];
  • riga 14: si indicano le cartelle contenenti le entità JPA. Ci sono quelle del progetto [intro-spring-data-01] e quelle del progetto del server sicuro. Queste informazioni sono oggetto del bean delle righe 16-19. Questo bean ridefinisce il bean omonimo del progetto [intro-spring-data-01]:

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

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

Nel livello [DAO], la riga 8 esegue la scansione delle cartelle indicate alla riga 1. A causa della ridefinizione del bean delle righe 14-17 nel progetto protetto (righe 16-19), la riga 8 sopra riportata eseguirà d’ora in poi la scansione delle cartelle ["spring.data.entities", "spring.security.entities"]. Si noti che la classe importata alla riga 10 dalla classe [spring.security.config.DaoConfig] deve includere l’annotazione [@Configuration]; in caso contrario, il meccanismo appena descritto non funziona.

La classe [SecurityConfig] configura gli aspetti di sicurezza del progetto. Abbiamo già incontrato una classe di configurazione di Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

Seguiremo lo stesso approccio:

  • riga 11: definire una classe che estende la classe [WebSecurityConfigurerAdapter];
  • riga 13: definire un metodo [configure(HttpSecurity http)] che definisce i diritti di accesso alle diverse URL del servizio web;
  • riga 19: definire un metodo [configure(AuthenticationManagerBuilder auth)] che definisce gli utenti e i loro ruoli;

La classe [SecurityConfig] sarà la seguente:


package spring.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import spring.security.dao.AppUserDetailsService;

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

    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // protezione
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // l'autenticazione viene effettuata dal bean [appUserDetailsService]
        // la password è crittografata tramite l'algoritmo di hash BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // applicazione sicura?
        if (activateSecurity) {
            // la password viene trasmessa tramite l'intestazione Authorization: Basic xxxx
            http.httpBasic();
            // il metodo HTTP OPTIONS deve essere autorizzato per tutti
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // solo il ruolo ADMIN può utilizzare l'applicazione
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // tutte le URL
                    .hasRole("ADMIN");
            // nessuna sessione
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • riga 16: per attivare gli elementi di Spring Security;
  • riga 17: si aggiungono i componenti Spring del pacchetto [spring.security.service];
  • riga 18: si importano i bean del livello [DAO] appena presentato, nonché quelli del server web non protetto / jSON;
  • righe 21-22: viene iniettata la classe [AppUserDetails] che consente l'accesso agli utenti dell'applicazione;
  • riga 25: un valore booleano che rende sicura (true) o meno (false) l’applicazione web;
  • righe 27-32: il metodo [configure(HttpSecurity http)] definisce gli utenti e i loro ruoli. Riceve come parametro un tipo [AuthenticationManagerBuilder]. Questo parametro viene arricchito con due informazioni (riga 38):
    • un riferimento al servizio [appUserDetailsService] della riga 22 che consente l’accesso agli utenti registrati. Si noti qui che il fatto che siano registrati in un database non risulta esplicito. Potrebbero quindi trovarsi in una cache, essere forniti da un servizio web, ...
    • il tipo di crittografia utilizzato per la password. Ricordiamo che abbiamo utilizzato l’algoritmo BCrypt;
  • righe 34-52: il metodo [configure(HttpSecurity http)] definisce i diritti di accesso ai URL del servizio web;
  • riga 37: abbiamo visto nel progetto introduttivo che, per impostazione predefinita, Spring Security gestiva un token CSRF (Cross Site Request Forgery) che l’utente che voleva autenticarsi doveva rinviare al server. Qui questo meccanismo è disattivato. Ciò, insieme al valore booleano (isSecured=false), consente di utilizzare l’applicazione web senza sicurezza;
  • riga 41: si attiva la modalità di autenticazione tramite l'intestazione HTTP. Il client dovrà inviare la seguente intestazione HTTP:
Authorization:Basic code

dove «code» è la codifica della stringa «login:password» tramite l’algoritmo Base64. Ad esempio, la codifica Base64 della stringa admin:admin è YWRtaW46YWRtaW4=. Pertanto, l'utente con nome utente [admin] e password [admin] invierà la seguente intestazione HTTP per autenticarsi:

Authorization:Basic YWRtaW46YWRtaW4=
  • righe 46-48: indicano che tutti i URL del servizio web sono accessibili agli utenti con il ruolo [ROLE_ADMIN]. Ciò significa che un utente che non possiede tale ruolo non può accedere al servizio web;
  • riga 50: in modalità [session], un utente che si è autenticato una volta non deve farlo per gli accessi successivi. Qui si disattiva questa modalità, pertanto l'utente dovrà autenticarsi ad ogni accesso;

16.4.8. Test del livello [DAO]

  

Per prima cosa, creiamo una classe eseguibile [CreateUser] in grado di creare un utente con un ruolo:


package sprin.security.tests;

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

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

public class CreateUser {

    public static void main(String[] args) {
        // sintassi: login password roleName

        // sono necessari tre parametri
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // si recuperano i parametri
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // contesto Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // il ruolo esiste già?
        Role role = roleRepository.findRoleByName(roleName);
        // se non esiste, lo si crea
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // L'utente esiste già?
        User user = userRepository.findUserByLogin(login);
        // se non esiste, lo creiamo
        if (user == null) {
            // si esegue l'hash della password con bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // si salva l'utente
            user = userRepository.save(new User(login, login, crypt));
            // si crea il collegamento con il ruolo
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // l'utente esiste già: possiede il ruolo richiesto?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // se non viene trovato, si crea la relazione con il ruolo
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // chiusura del contesto Spring
        context.close();
        // fine
        System.out.println("Travail terminé...");
    }

}
  • riga 17: la classe richiede tre argomenti che definiscono un utente: il suo nome utente, la sua password e il suo ruolo;
  • righe 25-27: i tre parametri vengono recuperati;
  • riga 29: il contesto Spring viene costruito a partire dalla classe di configurazione [AppConfig];
  • righe 30-32: si recuperano i riferimenti delle tre classi [Repository] che possono essere utili per creare l'utente;
  • riga 34: si verifica se il ruolo esiste già;
  • righe 36-38: se non esiste, lo si crea nel database. Avrà un nome del tipo [ROLE_XX];
  • riga 40: si verifica se il login esiste già;
  • righe 42-49: se il nome utente non esiste, lo creiamo nel database;
  • riga 44: si crittografa la password. Qui si utilizza la classe [BCrypt] di Spring Security (riga 4). Sono quindi necessari i file di questo framework. Il file [pom.xml] include questa dipendenza:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • riga 46: l'utente viene salvato nel database;
  • riga 48: così come la relazione che lo collega al suo ruolo;
  • righe 51-57: caso in cui il login esista già – si verifica quindi se tra i suoi ruoli sia già presente quello che si desidera assegnargli;
  • righe 59-61: se il ruolo cercato non è stato trovato, si crea una riga nella tabella [USERS_ROLES] per collegare l’utente al suo ruolo;
  • non ci si è protetti da eventuali eccezioni. Si tratta di una classe di supporto per creare rapidamente un utente con un ruolo.

Quando si esegue la classe con gli argomenti [x x guest], si ottengono fondamentalmente i seguenti risultati:

Tabella [USERS]

Tabella

Tabella [ROLES]

 

Tabella [USERS_ROLES]

 

Consideriamo ora la seconda classe [UsersTest], che è un test JUnit:

  

package spring.security.tests;

import java.util.List;

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

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

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

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

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

    // mappatore jSON
    private ObjectMapper mapper = new ObjectMapper();

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

    @Test
    public void findUserByLogin() {
        // si recupera l'utente [admin]
        User user = userRepository.findUserByLogin("admin");
        // si verifica che la sua password sia [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // si verifica il ruolo di admin / admin
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // si recupera l'utente [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // si verifica che la sua password sia [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // si verifica il ruolo di admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // metodo di utilità - visualizza gli elementi di una collezione
    private void display(String message, Iterable<?> elements) throws JsonProcessingException {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(mapper.writeValueAsString(element));
        }
    }
}
  • righe 37-44: test visivo. Vengono visualizzati tutti gli utenti con i relativi ruoli;
  • righe 46-56: si verifica che l’utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando il repository [UserRepository];
  • riga 51: [admin] è la password in chiaro. Nella base dati, è crittografata secondo l’algoritmo BCrypt. Il metodo [BCrypt.checkpw] consente di verificare che la password in chiaro, una volta crittografata, sia effettivamente uguale a quella presente nella base dati;
  • righe 58-69: si verifica che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando il servizio [appUserDetailsService];

L'esecuzione dei test ha esito positivo con i seguenti log:

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

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

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

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

16.4.9. Test del servizio web

Testeremo il servizio web con il client Chrome [Advanced Rest Client]. Dovremo specificare l'intestazione di autenticazione HTTP:

Authorization:Basic code

dove [code] è il codice Base64 della stringa [login:password]. Per generare questo codice, è possibile utilizzare il seguente programma:

  

package spring.security.helpers;

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

public class Base64Encoder {

    public static void main(String[] args) {
        // si attendono due argomenti: login e password
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // si recuperano i due argomenti
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // si codifica la stringa
        byte[] data = Base64.encode(chaîne.getBytes());
        // visualizza la sua codifica Base64
        System.out.println(new String(data));
    }

}

Se eseguiamo questo programma con i due argomenti [admin admin]:

  

si ottiene il seguente risultato:

YWRtaW46YWRtaW4=

Ora che sappiamo come generare l'intestazione di autenticazione HTTP, avviamo il servizio web protetto, quindi con il client Chrome [Advanced Rest Client] richiediamo l'elenco di tutti i prodotti:

  • in [1], richiediamo l’URL delle categorie;
  • in [2], con un metodo GET;
  • da [3], forniamo l'intestazione HTTP dell'autenticazione. Il codice [YWRtaW46YWRtaW4=] è la codifica Base64 della stringa [admin:admin];
  • in [4], inviamo il comando HTTP;

La risposta del server è la seguente:

  • in [1], l'intestazione di autenticazione HTTP;
  • in [2], il server restituisce una risposta jSON;

Si ottiene effettivamente l'elenco delle categorie:

 

Proviamo ora a inviare una richiesta HTTP con un'intestazione di autenticazione errata. La risposta è quindi la seguente:

  • in [1]: l'intestazione di autenticazione HTTP;

Otteniamo la seguente risposta:

  • in [2]: la risposta del servizio web;

Ora proviamo con l'utente user / user. Esiste ma non ha accesso al servizio web. Se eseguiamo il programma di codifica Base64 con i due argomenti [user user]:

  

otteniamo il seguente risultato:

dXNlcjp1c2Vy
  • in [1]: intestazione di autenticazione errata HTTP;
  • in [2]: la risposta del servizio web. È diversa dalla precedente, che era [401 Unauthorized]. Questa volta l'utente si è autenticato correttamente ma non dispone dei diritti sufficienti per accedere a URL;

Il nostro servizio web protetto è ora operativo.

16.4.10. Un codice di autenticazione URL

  

Creeremo un URL che ci consentirà di verificare se un utente è autorizzato o meno ad accedere al servizio web. A tal fine, creiamo il seguente nuovo controller MVC [AuthenticateController]:


package spring.security.service;

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

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

import spring.webjson.models.Response;

@Controller
public class AuthenticateController {

    // dipendenze Spring
    @Autowired
    private ApplicationContext context;

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

}
  • riga 15: la classe [AuthenticateController] è un controller Spring. In quanto tale, espone URL;
  • riga 22: espone il URL [/authenticate];
  • riga 23: il risultato del metodo verrà inviato direttamente al client;
  • righe 26-27: il metodo si limita a restituire un oggetto [Response] vuoto ma con un [status] pari a 0, a indicare che non si è verificato alcun errore;

A cosa serve questo URL? Quando vorremo semplicemente autenticare un utente, lo richiederemo. Abbiamo visto che se il livello di sicurezza non accetta quell’utente, restituisce un’eccezione. Ecco un esempio;

Con l’utente [admin:admin]:

Si ottiene una risposta vuota ma nessuna eccezione.

Con l’utente [user:user]:

Si è verificata un'eccezione.

16.4.11. Conclusione

L'aggiunta delle classi necessarie a Spring Security è stata possibile senza modifiche al progetto web/json originale. Questo caso particolarmente favorevole deriva dal fatto che le tre tabelle aggiunte al database sono indipendenti da quelle esistenti. Si sarebbe potuto persino inserirle in un database separato. In altri casi, le tabelle aggiunte possono avere relazioni con quelle esistenti. In tal caso è necessario modificare le entità JPA, il che in genere ha un impatto su tutti i livelli del progetto.

16.5. Un client programmato per il servizio web / jSON protetto

Abbiamo già scritto un client per il servizio web / jSON non protetto:

Ora creeremo un client programmato per il servizio web protetto:

Duplichiamo il progetto già scritto [intro-webjson-client] in un nuovo progetto [intro-spring-security-client-01]:

  

16.5.1. La classe [AbstractDao]

La classe [AbstractDao] garantisce la comunicazione HTTP con il server web / jSON protetto. Come abbiamo appena visto, in questa comunicazione HTTP, il client deve ora inviare un'intestazione di autenticazione, ad esempio:

Authorization:Basic YWRtaW46YWRtaW4=

Ciò avviene nel modo seguente:


package spring.security.client.dao;

import java.net.URI;
...

public abstract class AbstractDao {

    // dati
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // richiesta generica
    protected String getResponse(User user, String url, String jsonPost) {

// URL: URL da contattare

  • riga 15: il metodo generico [getResponse], responsabile della comunicazione HTTP con il servizio web protetto, ora accetta come primo parametro l’utente che richiede un URL. La classe [User] è la seguente:

Questa classe è la seguente:

  

package spring.security.client.entities;

public class User {

    // proprietà
    private String login;
    private String password;

    // costruttore
    public User() {
    }

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

    // getter e setter
...
}

Il metodo [getResponse] diventa quindi il seguente:


package spring.security.client.dao;

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

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

import spring.security.client.entities.User;

public abstract class AbstractDao {

    // dati
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    private String getBase64(User user) {
        // l'utente e la sua password vengono codificati in base 64 - richiede Java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }

    // richiesta generica
    protected String getResponse(User user, String url, String jsonPost) {

        // URL: URL da contattare
        // jsonPost: il valore jSON da inviare
        try {
            // esecuzione della richiesta
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }

            // si esegue la richiesta
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }

}

  • righe 42-44, 49-51: se l'utente [user] non è nullo, allora si aggiunge l'intestazione di autenticazione. La codifica Base64 dell'utente e della sua password è garantita dal metodo [getBase64] delle righe 25-29. Si noti che questo metodo utilizza una classe [Base64] appartenente a JDK 1.8.
  • A parte le righe precedenti, il codice rimane invariato;

16.5.2. L'interfaccia [IDao]

Tutti i metodi dell’interfaccia [IDao] ricevono un parametro aggiuntivo [User user]:

  

package spring.security.client.dao;

import java.util.List;

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

public interface IDaoClient {

    // autenticazione
    public void authenticate(User user);

    // inserimento di un elenco di prodotti
    public List<Produit> addProduits(User user, List<Produit> produits);

    // eliminazione di tutti i prodotti
    public void deleteAllProduits(User user);

    // aggiornamento di un elenco di prodotti
    public List<Produit> updateProduits(User user, List<Produit> produits);

    // recupero di tutti i prodotti
    public List<Produit> getAllProduits(User user);

    // inserimento di un elenco di categorie
    public List<Categorie> addCategories(User user, List<Categorie> categories);

    // eliminazione di tutte le categorie
    public void deleteAllCategories(User user);

    // aggiornamento di un elenco di categorie
    public List<Categorie> updateCategories(User user, List<Categorie> categories);

    // recupero di tutte le categorie
    public List<Categorie> getAllCategories(User user);

    // un prodotto specifico
    public Produit getProduitByIdWithCategorie(User user, Long idProduit);

    public Produit getProduitByIdWithoutCategorie(User user, Long idProduit);

    public Produit getProduitByNameWithCategorie(User user, String nom);

    public Produit getProduitByNameWithoutCategorie(User user, String nom);

    // una categoria specifica
    public Categorie getCategorieByIdWithProduits(User user, Long idCategorie);

    public Categorie getCategorieByIdWithoutProduits(User user, Long idCategorie);

    public Categorie getCategorieByNameWithProduits(User user, String nom);

    public Categorie getCategorieByNameWithoutProduits(User user, String nom);

}
  • riga 12: abbiamo aggiunto il metodo [authenticate(User user)] per autenticare un utente. Esso genera un'eccezione se l'utente non dispone dei diritti di accesso a URL [/authenticate] del servizio web;

16.5.3. La classe [Dao]

Tutti i metodi della classe [Dao] ricevono un parametro aggiuntivo [User user] che passano al metodo generico [getResponse] della classe [AbstractDao]. Ecco due esempi:


// autenticazione
    @Override
    public void authenticate(User user) {
        getResponse(user, "/authenticate", null);
    }

    @Override
    public List<Produit> addProduits(User user, List<Produit> produits) {
        // ----------- aggiunta di prodotti (senza la relativa categoria)
        try {
            // mappatori jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // richiesta
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // errore?
            if (response.getStatus() != 0) {
                // viene generata 1 eccezione
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // viene restituito il corpo della risposta del server
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (IOException | RuntimeException e2) {
            throw new DaoException(100, e2);
        }
    }

16.5.4. Test unitari della classe [Dao]

La classe [Test01] dei test unitari della classe [Dao] viene modificata come segue:

  

package client.tests.junit;

...

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

    // contesto Spring
    @Autowired
    private ApplicationContext context;
    // livello [DAO]
    @Autowired
    private IDaoClient dao;

    // utenti
    static private User admin;
    static private User user;
    static private User unknown;

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

    @Before
    public void cleanAndFill() {
        // si pulisce il database prima di ogni test
        log("Vidage de la base de données", 1);
        // si svuota la tabella [CATEGORIES] - a cascata verrà svuotata la tabella [PRODUITS]
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // si compilano le tabelle
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // si aggiunge la categoria - per effetto a cascata verranno inseriti anche i prodotti
        dao.addCategories(admin, categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // elenco delle categorie
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories(admin);
        affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
        // elenco dei prodotti
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits(admin);
        affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
        // alcune verifiche
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }
...
    @Test()
    public void checkUserUser() {
        ServiceException se = null;
        try {
            dao.authenticate(user);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getMessages().get(0));
    }

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

    @Test()
    public void checkUserAdmin() {
        ServiceException se = null;
        try {
            dao.authenticate(admin);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNull(se);
    }
...
}
  • durante l’inizializzazione della classe di test, righe 21-26, vengono creati tre utenti:
    • l’utente [admin] ha accesso a URL del servizio web, test righe 96-104;
    • l'utente [user] esiste ma non è autorizzato a utilizzare i URL del servizio web, test righe 71-81;
    • l'utente [unknown] non esiste, test righe 83-93;
  • i metodi di test sono quelli già visti per il servizio web non protetto, con la differenza che i metodi dell'interfaccia [IDaoClient] vengono chiamati con come primo parametro l'utente [admin], che ha il diritto di utilizzare i URL;

Il test va a buon fine, ma si nota che è più lento rispetto al servizio web non protetto. La protezione di un’applicazione ne aumenta sensibilmente i tempi di risposta. Si può notare un fattore importante nelle prestazioni del servizio web protetto: nella classe [AppConfig] che lo configura, abbiamo scritto:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // applicazione sicura?
        if (activateSecurity) {
            // la password viene trasmessa tramite l’intestazione Authorization: Basic xxxx
            http.httpBasic();
            // il metodo HTTP OPTIONS deve essere autorizzato per tutti
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // solo il ruolo ADMIN può utilizzare l’applicazione
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // tutte le URL
                    .hasRole("ADMIN");
            // nessuna sessione
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

La riga 17 comporta un costo. Obbliga l’utente ad autenticarsi ad ogni accesso. Se la si mette tra commenti, la durata del test JUnit precedente passa da 10,57 secondi a 4,21 secondi, questo perché l’utente [admin] effettua l’autenticazione solo per il primo test e non per quelli successivi (anche se l’intestazione di autenticazione HTTP viene inviata dal client, il server non verifica nuovamente la password dell’utente). Con un servizio web non protetto, la durata del test JUnit scende a 2,33 secondi.