16. [Corso]: Protezione dell'accesso a un servizio web con Spring Security
Parole chiave: architettura multistrato, Spring, iniezione di dipendenze, servizio web sicuro / JSON, client / server
16.1. Assistenza
![]() | ![]() |
I progetti relativi a 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. Molto spesso, essa sarà basata 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
Importeremo nuovamente una guida Spring seguendo i passaggi da 1 a 3 riportati di seguito:
![]() |
![]() |
Il progetto comprende i seguenti elementi:
- nella cartella [templates] troverete le pagine HTML del progetto;
- [Application]: è la classe eseguibile del progetto;
- [MvcConfig]: è la classe di configurazione 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 vedere 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. 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. Questo è il nome dell'utente che ha effettuato l'accesso;
- 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}"] garantisce 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}"] garantisce che il tag <div> venga generato solo se l'URL che visualizza la pagina di accesso contiene il parametro [logout] (http://context/login?logout);
- righe 11–23: un modulo HTML;
- riga 11: il modulo verrà inviato all'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 alla riga 28 che Thymeleaf ha aggiunto un campo nascosto denominato [_csrf].
16.3.3. Configurazione di 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 sovrascrivere determinati metodi;
- riga 10: ridefinizione di un metodo della classe padre;
- righe 11–16: il metodo [addViewControllers] consente di associare gli URL alle viste HTML. Qui vengono effettuate le seguenti associazioni:
vista | |
/templates/home.html | |
/templates/hello.html | |
/modelli/login.html |
Il suffisso [html] e la cartella [templates] sono i valori predefiniti utilizzati da Thymeleaf. Possono essere modificati tramite la configurazione. La cartella [templates] deve trovarsi nella radice del classpath del progetto:
![]() |
Nel punto [1] sopra, le cartelle [java] e [resources] sono entrambe cartelle sorgente. Ciò significa che il loro contenuto si troverà nella radice del classpath del progetto. Pertanto, nel punto [2], le cartelle [hello] e [templates] si troveranno nella 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] rende la classe [WebSecurityConfig] una classe di configurazione di Spring Security;
- riga 11: la classe [WebSecurity] estende la classe [WebSecurityConfigurerAdapter] per sovrascrivere determinati metodi;
- riga 12: ridefinizione di un metodo della classe padre;
- righe 13–16: il metodo [configure(HttpSecurity http)] viene sovrascritto per definire i diritti di accesso per i vari URL dell'applicazione;
- riga 14: il metodo [http.authorizeRequests()] consente di associare gli URL ai diritti di accesso. Qui vengono effettuate le seguenti associazioni:
regola | codice | |
accesso senza autenticazione | | |
solo accesso autenticato |
- riga 15: definisce il metodo di autenticazione. L'autenticazione viene eseguita tramite un modulo URL [/login] accessibile a tutti [http.formLogin().loginPage("/login").permitAll()]. Anche il logout è accessibile a tutti;
- righe 19–21: ridefiniscono il metodo [configure(AuthenticationManagerBuilder auth)] che gestisce gli utenti;
- riga 20: l'autenticazione viene eseguita utilizzando utenti hardcoded [auth.inMemoryAuthentication()]. Qui viene definito un utente con il login [user], la password [password] e il ruolo [USER]. Agli utenti con lo stesso ruolo possono essere concesse le stesse autorizzazioni;
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] indica a Spring Boot (riga 3) di eseguire la configurazione che lo sviluppatore non ha impostato esplicitamente;
- riga 9: rende la classe [Application] una classe di configurazione Spring;
- riga 10: indica al sistema di scansionare la directory contenente la classe [Application] per cercare i componenti Spring. Le due classi [MvcConfig] e [WebSecurityConfig] verranno quindi individuate poiché presentano l'annotazione [@Configuration];
- riga 13: il metodo [main] della classe eseguibile;
- riga 14: il metodo statico [SpringApplication.run] viene eseguito con la classe di configurazione [Application] come parametro. Abbiamo già incontrato questo processo e sappiamo che il server Tomcat incorporato nelle dipendenze Maven del progetto verrà avviato e il progetto distribuito su di esso. Abbiamo visto che quattro URL erano gestiti [/, /home, /login, /hello] e che alcuni erano protetti da diritti di accesso.
16.3.6. Test dell'applicazione
Iniziamo richiedendo l'URL [/], che è uno dei quattro URL accettati. È associato alla vista [/templates/home.html]:
![]() |
L'URL richiesto [/] è accessibile a tutti. Ecco perché siamo riusciti a recuperarlo. Il link [qui] è il seguente:
L'URL [/hello] verrà richiesto quando clicchiamo sul link. Questo è protetto:
regola | codice | |
accesso senza autenticazione | | |
solo accesso autenticato |
Per accedervi è necessario essere autenticati. Spring Security reindirizzerà quindi il browser del client alla pagina di autenticazione. In base alla configurazione mostrata, si tratta della pagina all'URL [/login]. Questa pagina è accessibile a tutti:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Quindi otteniamo [1]:
![]() |
Il codice sorgente della pagina ottenuta è il seguente:
- alla riga 7, compare un campo nascosto che non è presente nella pagina originale [login.html]. È stato aggiunto da Thymeleaf. Questo codice, noto come CSRF (Cross-Site Request Forgery), è progettato per eliminare una vulnerabilità di sicurezza. Questo token deve essere rinviato a Spring Security insieme all'autenticazione affinché venga accettato;
Ricordiamo che solo la coppia utente/password viene riconosciuta 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 all'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 utente/password [4]:
![]() |
- in [4], effettuiamo il login;
- in [5], Spring Security ci reindirizza all'URL [/hello] perché quello è l'URL che abbiamo richiesto quando siamo stati reindirizzati alla pagina di accesso. 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 [Esci], viene inviata una richiesta POST all'URL [/logout]. Come l'URL [/login], questo URL è accessibile a tutti:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Nella nostra mappatura URL/vista, non abbiamo definito nulla per l'URL [/logout]. Cosa succederà? Proviamo:
![]() |
- In [6], clicchiamo sul pulsante [Sign Out];
- in [7], vediamo che siamo stati reindirizzati all'URL [http://localhost:8080/login?logout]. Spring Security ha richiesto questo reindirizzamento. La presenza del parametro [logout] nell'URL ha fatto sì che nella vista venisse visualizzata la seguente riga:
<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 proteggerla in un secondo momento. Spring Security è non intrusivo. È possibile implementare la sicurezza per un'applicazione web che è già stata scritta. Inoltre, abbiamo scoperto i seguenti punti:
- è 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 un parametro di errore aggiuntivo nell'URL;
- se l'autenticazione ha esito positivo, si viene reindirizzati alla pagina richiesta al momento dell'autenticazione. Se si richiede la pagina di autenticazione direttamente senza passare attraverso una pagina intermedia, Spring Security reindirizza all'URL [/] (questo caso non è stato dimostrato);
- Si effettua il logout richiedendo l'URL [/logout] con una richiesta POST. Spring Security reindirizza quindi alla pagina di autenticazione con il parametro "logout" nell'URL;
Tutte queste conclusioni si basano sul comportamento predefinito di Spring Security. Questo comportamento può essere modificato tramite configurazione sovrascrivendo determinati metodi della classe [WebSecurityConfigurerAdapter].
Il tutorial precedente ci sarà di scarso aiuto per il prosieguo. Utilizzeremo infatti:
- un database per memorizzare gli utenti, le loro password e i loro ruoli;
- l'autenticazione basata su header HTTP;
Ci sono pochissimi tutorial disponibili per quello che vogliamo fare qui. La soluzione che proporremo è una combinazione di frammenti di codice trovati qua e là.
16.4. Implementazione della sicurezza sul servizio web del prodotto / JSON
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:

Tabella [USERS]: utenti
- ID: chiave primaria;
- VERSION: colonna di versioning delle righe;
- IDENTITY: identificatore descrittivo dell'utente;
- LOGIN: login dell'utente;
- PASSWORD: la sua password;
Nella tabella USERS, le password non sono memorizzate in chiaro:
![]() |
L'algoritmo utilizzato per crittografare le password è l'algoritmo BCRYPT.
Tabella [ROLES]: ruoli
- ID: chiave primaria;
- VERSION: colonna di versioning della riga;
- NAME: nome del ruolo. Per impostazione predefinita, Spring Security si aspetta nomi nel formato ROLE_XX, come ROLE_ADMIN o ROLE_GUEST;
![]() |
Tabella [USERS_ROLES]: tabella di join USERS/ROLES
Un utente può avere più ruoli e un ruolo può includere più utenti. Si tratta di una relazione molti-a-molti rappresentata dalla tabella [USERS_ROLES].
- ID: chiave primaria;
- VERSION: colonna di versioning delle righe;
- USER_ID: identificatore utente;
- ROLE_ID: identificatore del 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 repository Spring Data associati alle tre nuove tabelle;
- [spring.security.dao]: contiene un servizio basato sui [repository];
- [spring.security.config]: contiene la configurazione del progetto, compresa quella per l'accesso sicuro al servizio web;
- [spring.security.boot]: contiene la classe di avvio per il 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>
<!-- Spring logs -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- plugins -->
<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: riutilizziamo il codice esistente con il servizio web/archivio JSON che abbiamo esaminato;
- righe 29–32: la dipendenza che introduce le classi Spring Security;
- righe 34–37: la libreria di logging;
- righe 39–42: la libreria che consente l'uso delle annotazioni di Spring Boot;
- righe 44–48: la libreria necessaria per il testing;
16.4.4. Le nuove entità [JPA]
![]() |
Il livello JPA definisce tre nuove entità:
![]() |
La classe [User] rappresenta la 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 {
// properties
@Column(name = "NAME")
private String name;
@Column(name = "LOGIN")
private String login;
@Column(name = "PASSWORD")
private String password;
// manufacturer
public User() {
}
public User(String name, String login, String password) {
this.name = name;
this.login = login;
this.password = password;
}
// getters and setters
...
}
- riga 11: la classe estende la classe [AbstractEntity] già utilizzata per le altre entità;
La classe [Role] rappresenta la 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 {
// properties
@Column(name="NAME")
private String name;
// manufacturers
public Role() {
}
public Role(String name) {
this.name = name;
}
// getters and setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
La classe [UserRole] rappresenta la 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 {
// foreign keys
@Column(name = "USER_ID", insertable = false, updatable = false)
private Long userId;
@Column(name = "ROLE_ID", insertable = false, updatable = false)
private Long roleId;
// a UserRole refers to a User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// a UserRole refers to a Role
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// manufacturers
public UserRole() {
}
public UserRole(User user, Role role) {
this.user = user;
this.role = role;
}
// getters and setters
...
}
- 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. I [repository]
![]() |
Ciascuna delle entità JPA precedenti è 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> {
// liste des rôles d'un utilisateur identifié par son id
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// liste des rôles d'un utilisateur identifié par son login unique
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// recherche d'un utilisateur via son login
User findUserByLogin(String login);
}
- riga 9: l'interfaccia [UserRepository] estende l'interfaccia [CrudRepository] di Spring Data (riga 4);
- righe 12-13: il metodo [getRoles(User user)] recupera tutti i ruoli per un utente identificato dal proprio [id]
- righe 16-17: come sopra, ma per un utente identificato dal proprio login e password;
- riga 20: per trovare un utente tramite il suo nome utente;
L'interfaccia [RoleRepository] gestisce l'accesso 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> {
// search for a role by name
Role findRoleByName(String name);
}
- riga 7: l'interfaccia [RoleRepository] estende l'interfaccia [CrudRepository];
- riga 10: è possibile cercare un ruolo in base al suo nome;
L'interfaccia [UserRoleRepository] gestisce l'accesso 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] estende semplicemente l'interfaccia [CrudRepository] senza aggiungere alcun nuovo metodo;
16.4.6. Classi di gestione di utenti e ruoli
![]() |
![]() |
Spring Security richiede la creazione di una classe che implementi la seguente interfaccia [UsersDetail]:
![]() |
Questa interfaccia è implementata qui 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;
// properties
private User user;
private UserRepository userRepository;
// manufacturers
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
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;
}
// getters and setters
...
}
- riga 14: la classe [AppUserDetails] implementa l'interfaccia [UserDetails];
- righe 19–20: la classe incapsula un utente (riga 19) e il repository che fornisce i dettagli su quell'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 di un tipo derivato. Qui utilizziamo il tipo derivato [SimpleGrantedAuthority] (riga 36), che incapsula il nome di uno dei ruoli dell'utente della riga 19;
- righe 35–37: iteriamo attraverso l'elenco dei ruoli dell'utente della riga 19 per costruire un elenco di elementi di tipo [SimpleGrantedAuthority];
- righe 42–44: implementiamo il metodo [getPassword] dell'interfaccia [UserDetails]. Restituiamo la password dell'utente della riga 19;
- righe 42–44: implementiamo il metodo [getUserName] dell'interfaccia [UserDetails]. Restituiamo il nome utente dell'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 {
// search for user via login
User user = userRepository.findUserByLogin(login);
// found?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// render user details
return new AppUserDetails(user, userRepository);
}
}
- riga 12: la classe sarà un componente Spring, quindi sarà disponibile nel suo 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 utilizzando il suo nome utente;
- righe 23–25: se l'utente non viene trovato, viene generata un'eccezione;
- riga 27: viene costruito e restituito un oggetto [AppUserDetails]. È infatti di tipo [UserDetails] (riga 19);
16.4.7. 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 {
// constants
final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };
@Bean
public String[] packagesToScan() {
return ENTITIES_PACKAGES;
}
}
- Riga 10: importiamo la classe di configurazione [spring.data.config.DaoConfig] dal progetto [intro-spring-data-01], che implementa il livello [DAO] per prodotti e categorie;
- riga 8: specifichiamo le cartelle nel progetto corrente contenenti i [repository] di Spring Data;
- riga 9: specifichiamo le cartelle nel progetto corrente contenenti i componenti Spring relativi al livello [DAO];
- Riga 14: specifica le directory contenenti le entità JPA. Queste includono quelle del progetto [intro-spring-data-01] e quelle del progetto del server sicuro. Queste informazioni sono definite nel bean alle righe 16–19. Questo bean sovrascrive il bean con lo stesso nome nel 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 directory specificate alla riga 1. A causa della ridefinizione del bean alle righe 14–17 nel progetto secure (righe 16–19), la riga 8 sopra riportata ora eseguirà la scansione delle directory ["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 comportamento sopra descritto non funzionerà.
La classe [SecurityConfig] configura l'aspetto 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 la stessa procedura:
- riga 11: definire una classe che estende la classe [WebSecurityConfigurerAdapter];
- riga 13: definire un metodo [configure(HttpSecurity http)] che definisca i diritti di accesso ai vari 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;
// security
private boolean activateSecurity = true;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// authentication is performed by bean [appUserDetailsService]
// the password is encrypted using the BCrypt hash algorithm
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// secure application?
if (activateSecurity) {
// the password is transmitted by the header Authorization: Basic xxxx
http.httpBasic();
// the HTTP OPTIONS method must be authorized for all
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// only the ADMIN role can use the application
http.authorizeRequests() //
.antMatchers("/", "/**") // all URL
.hasRole("ADMIN");
// no session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- riga 16: per abilitare i componenti di Spring Security;
- riga 17: aggiungiamo i componenti Spring del pacchetto [spring.security.service];
- riga 18: importiamo i bean dal livello [DAO] che abbiamo appena introdotto, così come quelli dal server web non protetto/JSON;
- righe 21–22: viene iniettata la classe [AppUserDetails], che fornisce l'accesso agli utenti dell'applicazione;
- riga 25: un valore booleano che protegge (true) o non protegge (false) l'applicazione web;
- righe 27–32: il metodo [configure(HttpSecurity http)] definisce gli utenti e i loro ruoli. Accetta come parametro un tipo [AuthenticationManagerBuilder]. Questo parametro viene arricchito con due informazioni (riga 38):
- un riferimento a [appUserDetailsService] della riga 22, che fornisce l'accesso agli utenti registrati. Si noti qui che il fatto che siano memorizzati in un database non è esplicitamente dichiarato. Potrebbero quindi trovarsi in una cache, essere forniti da un servizio web, ecc.
- 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 agli URL del servizio web;
- riga 37: abbiamo visto nel progetto introduttivo che, per impostazione predefinita, Spring Security gestisce un token CSRF (Cross-Site Request Forgery) che l'utente che tenta di autenticarsi deve inviare al server. Qui, questo meccanismo è disabilitato. In combinazione con il valore booleano (isSecured=false), ciò consente di utilizzare l'applicazione web senza sicurezza;
- riga 41: abilitiamo l'autenticazione tramite header HTTP. Il client deve inviare il seguente header HTTP:
dove code è la codifica Base64 della stringa login:password. Ad esempio, la codifica Base64 della stringa admin:admin è YWRtaW46YWRtaW4=. Pertanto, un utente con login [admin] e password [admin] invierà la seguente intestazione HTTP per autenticarsi:
- Righe 46–48: specificano che tutti gli URL del servizio web sono accessibili agli utenti con il ruolo [ROLE_ADMIN]. Ciò significa che un utente senza questo ruolo non può accedere al servizio web;
- Riga 50: in modalità [session], un utente che si è autenticato una volta non ha bisogno di farlo per gli accessi successivi. Qui disabilitiamo questa modalità, quindi l'utente dovrà autenticarsi ogni volta che accede al servizio;
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) {
// syntax: login password roleName
// three parameters are required
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// parameters are retrieved
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// spring context
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// does the role already exist?
Role role = roleRepository.findRoleByName(roleName);
// if it doesn't exist, we create it
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// does the user already exist?
User user = userRepository.findUserByLogin(login);
// if it doesn't exist, we create it
if (user == null) {
// hash the password with bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// save user
user = userRepository.save(new User(login, login, crypt));
// we create the relationship with the role
userRoleRepository.save(new UserRole(user, role));
} else {
// the user already exists - does he/she have the required role?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// if not found, we create the relationship with the role
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// closing Spring context
context.close();
// end
System.out.println("Travail terminé...");
}
}
- riga 17: la classe richiede tre argomenti che definiscono un utente: login, password e ruolo;
- righe 25–27: i tre parametri vengono recuperati;
- riga 29: il contesto Spring viene costruito dalla classe di configurazione [AppConfig];
- righe 30–32: vengono recuperati i riferimenti ai tre oggetti [Repository] che potrebbero essere utili per la creazione dell'utente;
- riga 34: si verifica se il ruolo esiste già;
- righe 36–38: in caso contrario, lo creiamo 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: crittografiamo la password. Qui utilizziamo la classe [BCrypt] di Spring Security (riga 4). Abbiamo quindi bisogno degli archivi per 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: se l'account esiste già, verifichiamo se il ruolo che vogliamo assegnargli è già tra i suoi ruoli;
- Righe 59–61: se il ruolo cercato non viene trovato, viene creata una riga nella tabella [USERS_ROLES] per collegare l'utente al proprio ruolo;
- Non abbiamo previsto alcuna protezione contro potenziali eccezioni. Si tratta di una classe di supporto per la creazione rapida di un utente con un ruolo.
Quando la classe viene eseguita con gli argomenti [x x guest], si ottengono i seguenti risultati nel database:
Tabella [USERS]
![]() |
Tabella [RUOLI]
![]() |
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;
// mapper 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() {
// user [admin] is retrieved
User user = userRepository.findUserByLogin("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// check admin / admin role
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() {
// user [admin] is retrieved
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// check admin / admin role
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// utility method - displays items in a collection
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. Visualizziamo tutti gli utenti insieme ai loro ruoli;
- righe 46–56: verifichiamo che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando [UserRepository];
- riga 51: [admin] è la password in chiaro. Nel database, è crittografata utilizzando l'algoritmo BCrypt. Il metodo [BCrypt.checkpw] verifica che la password in chiaro crittografata corrisponda a quella nel database;
- righe 58–69: verifichiamo che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando [appUserDetailsService];
I test vengono eseguiti con successo con i seguenti log:
16.4.9. Test del servizio web
Testeremo il servizio web utilizzando il client Chrome [Advanced Rest Client]. Dovremo specificare l'intestazione di autenticazione HTTP:
dove [codice] è la stringa [login:password] codificata in Base64. 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) {
// we expect two arguments: login password
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// we retrieve the two arguments
String chaîne = String.format("%s:%s", args[0], args[1]);
// encode the string
byte[] data = Base64.encode(chaîne.getBytes());
// displays its Base64 encoding
System.out.println(new String(data));
}
}
Se eseguiamo questo programma con i due argomenti [admin admin]:
![]() |
otteniamo il seguente risultato:
Ora che sappiamo come generare l'intestazione di autenticazione HTTP, avviamo il servizio web sicuro, quindi utilizzando il client Chrome [Advanced Rest Client], richiediamo l'elenco di tutti i prodotti:
![]() |
- in [1], richiediamo l'URL delle categorie;
- in [2], utilizzando un metodo GET;
- in [3], forniamo l'intestazione di autenticazione HTTP. Il codice [YWRtaW46YWRtaW4=] è la codifica Base64 della stringa [admin:admin];
- in [4], inviamo la richiesta HTTP;
La risposta del server è la seguente:
![]() |
- in [1], l'intestazione di autenticazione HTTP;
- in [2], il server restituisce una risposta JSON;
Otteniamo con successo l'elenco delle categorie:
![]() |
Ora proviamo a inviare una richiesta HTTP con un'intestazione di autenticazione errata. La risposta è quindi la seguente:
![]() |
- in [1]: l'intestazione di autenticazione HTTP;
Riceviamo la seguente risposta:
![]() |
- in [2]: la risposta del servizio web;
Ora proviamo con l'utente / 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:
![]() |
- in [1]: l'intestazione di autenticazione HTTP errata;
![]() |
- in [2]: la risposta del servizio web. È diversa dalla precedente, che era [401 Non autorizzato]. Questa volta, l'utente si è autenticato correttamente ma non dispone delle autorizzazioni sufficienti per accedere all'URL;
Il nostro servizio web sicuro è ora operativo.
16.4.10. Un URL di autenticazione
![]() |
Creeremo un URL che ci consentirà di determinare se un utente è autorizzato 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 {
// spring dependencies
@Autowired
private ApplicationContext context;
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {
// answer 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 gli URL;
- riga 22: espone l'URL [/authenticate];
- riga 23: il risultato del metodo verrà inviato direttamente al client;
- righe 26–27: il metodo restituisce semplicemente un oggetto [Response] vuoto, ma con uno [status] pari a 0, a indicare che non si è verificato alcun errore;
A cosa serve questo URL? Lo richiederemo quando vorremo semplicemente autenticare un utente. Abbiamo visto che se il livello di sicurezza non accetta questo utente, genera un'eccezione. Ecco un esempio;
Con l'utente [admin:admin]:
![]() | ![]() |
Otteniamo una risposta vuota ma nessuna eccezione.
Con l'utente [user:user]:
![]() | ![]() |
Si è verificata un'eccezione.
16.4.11. Conclusione
Le classi necessarie per Spring Security sono state aggiunte senza modificare il progetto web/JSON originale. Questo scenario molto favorevole deriva dal fatto che le tre tabelle aggiunte al database sono indipendenti dalle tabelle esistenti. Avremmo potuto persino collocarle in un database separato. In altri casi, le tabelle aggiunte potrebbero avere relazioni con tabelle esistenti. In tal caso, le entità JPA devono essere modificate, il che generalmente ha un impatto su tutti i livelli del progetto.
16.5. Un client programmato per il servizio web/JSON sicuro
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 esistente [intro-webjson-client] in un nuovo progetto [intro-spring-security-client-01]:
![]() |
16.5.1. La classe [AbstractDao]
La classe [AbstractDao] gestisce la comunicazione HTTP con il server web/JSON sicuro. Come abbiamo appena visto, in questa comunicazione HTTP, il client deve ora inviare un'intestazione di autenticazione, ad esempio:
Questo viene fatto come segue:
package spring.security.client.dao;
import java.net.URI;
...
public abstract class AbstractDao {
// data
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// generic request
protected String getResponse(User user, String url, String jsonPost) {
// url : URL to contact
- riga 15: il metodo generico [getResponse], responsabile della comunicazione HTTP con il servizio web sicuro, 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 {
// properties
private String login;
private String password;
// manufacturer
public User() {
}
public User(String login, String password) {
this.login = login;
this.password = password;
}
// getters and setters
...
}
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 {
// data
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
private String getBase64(User user) {
// encodes user and password in base 64 - requires 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())));
}
// generic request
protected String getResponse(User user, String url, String jsonPost) {
// url : URL to contact
// jsonPost: the jSON value to be posted
try {
// request execution
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);
}
// execute the query
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 non è nullo, viene aggiunto l'header di autenticazione. La codifica Base64 dell'utente e della sua password è gestita dal metodo [getBase64] nelle righe 25–29. Si noti che questo metodo utilizza una classe [Base64] appartenente al 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 {
// authentication
public void authenticate(User user);
// insert product list
public List<Produit> addProduits(User user, List<Produit> produits);
// removal of all products
public void deleteAllProduits(User user);
// product list update
public List<Produit> updateProduits(User user, List<Produit> produits);
// all products obtained
public List<Produit> getAllProduits(User user);
// inserting a list of categories
public List<Categorie> addCategories(User user, List<Categorie> categories);
// delete all categories
public void deleteAllCategories(User user);
// updating a list of categories
public List<Categorie> updateCategories(User user, List<Categorie> categories);
// obtaining all categories
public List<Categorie> getAllCategories(User user);
// a special product
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);
// a special category
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. Genera un'eccezione se l'utente non dispone dell'autorizzazione per accedere all'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:
// authentication
@Override
public void authenticate(User user) {
getResponse(user, "/authenticate", null);
}
@Override
public List<Produit> addProduits(User user, List<Produit> produits) {
// ----------- add products (without category)
try {
// mappers jSON
ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
ObjectMapper mapperResponse = mapperPost;
// request
Response<List<Produit>> response = mapperResponse.readValue(
getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
new TypeReference<Response<List<Produit>>>() {
});
// mistake?
if (response.getStatus() != 0) {
// 1 exception is thrown
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// render the core of the server response
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (IOException | RuntimeException e2) {
throw new DaoException(100, e2);
}
}
16.5.4. Test unitari per la classe [Dao]
La classe [Test01] per il test unitario della classe [Dao] viene modificata come segue:
![]() |
package client.tests.junit;
...
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// spring context
@Autowired
private ApplicationContext context;
// layer [DAO]
@Autowired
private IDaoClient dao;
// users
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() {
// the base is cleaned before each test
log("Vidage de la base de données", 1);
// table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
dao.deleteAllCategories(admin);
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// fill the tables
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);
}
// add the category - the products will be cascaded in as well
dao.addCategories(admin, categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// list of categories
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories(admin);
affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
// product list
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits(admin);
affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
// a few checks
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 agli URL del servizio web, righe di test 96–104;
- l'utente [user] esiste ma non è autorizzato a utilizzare gli URL del servizio web, righe di test 71–81;
- l'utente [unknown] non esiste, righe di test 83–93;
- i metodi di test sono gli stessi già visti per il servizio web non protetto, tranne per il fatto che i metodi dell'interfaccia [IDaoClient] vengono chiamati con l'utente [admin] — che ha il permesso di utilizzare gli URL — come primo parametro;
Il test viene superato, ma possiamo notare che è più lento rispetto al servizio web non protetto. La protezione di un'applicazione aumenta significativamente i suoi tempi di risposta. C'è un fattore importante che influisce sulle 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();
// secure application?
if (activateSecurity) {
// the password is transmitted by the header Authorization: Basic xxxx
http.httpBasic();
// the HTTP OPTIONS method must be authorized for all
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// only the ADMIN role can use the application
http.authorizeRequests() //
.antMatchers("/", "/**") // all URL
.hasRole("ADMIN");
// no session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
La riga 17 ha un costo. Obbliga l'utente ad autenticarsi ad ogni accesso. Se la commentiamo, la durata del precedente test JUnit scende da 10,57 secondi a 4,21 secondi, poiché l'utente [admin] si autentica 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.
























































