Skip to content

19. Protezione dell'accesso a un servizio web con Spring Security

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

Inquadriamo Spring Security nello sviluppo di un'applicazione web. Molto spesso, essa 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.

19.2. Un tutorial su Spring Security

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

19.2.1. Configurazione Maven

Il progetto [3] è un progetto Maven. Diamo un'occhiata al 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;

19.2.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 "Ciao [[${#httpServletRequest.remoteUser}]]!";
  • riga 9: la traduzione di @{/logout};
  • riga 11: un campo nascosto denominato (attributo name) _csrf;

La vista finale [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 accesso 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].

19.2.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 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:
URL
vista
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/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.

19.2.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:
URL
regola
codice
/, /home
accesso senza autenticazione

http.authorizeRequests().antMatchers("/", "/home").permitAll()
altri URL
solo accesso autenticato
http.anyRequest().authenticated();
  • 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()]. Un utente viene definito qui con il login [user], la password [password] e il ruolo [USER]. Agli utenti con lo stesso ruolo possono essere concesse le stesse autorizzazioni;

19.2.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 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 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 verrà distribuito su di esso. Abbiamo visto che sono stati gestiti quattro URL [/, /home, /login, /hello] e che alcuni erano protetti da diritti di accesso.

19.2.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:

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

L'URL [/hello] verrà richiesto quando clicchiamo sul link. Questo è protetto:

URL
regola
codice
/, /home
accesso senza autenticazione

http.authorizeRequests().antMatchers("/", "/home").permitAll()
altri URL
solo accesso autenticato
http.anyRequest().authenticated();

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:

<!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, noto come CSRF (Cross-Site Request Forgery), è progettato per risolvere una vulnerabilità di sicurezza. Questo token deve essere rinviato a Spring Security insieme alle credenziali di autenticazione affinché l'autenticazione venga accettata;

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 nome utente e 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 [Esci];
  • 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>

19.2.7. Conclusione

Nell'esempio precedente, avremmo potuto scrivere prima l'applicazione web e poi proteggerla. 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. È possibile modificare tale comportamento tramite la configurazione, sovrascrivendo determinati metodi della classe [WebSecurityConfigurerAdapter].

Il tutorial precedente ci sarà di scarso aiuto d'ora in poi. 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à.