Skip to content

19. De toegang tot een webservice beveiligen met Spring Security

19.1. De rol van Spring Security in een webapplicatie

Laten we Spring Security in de context van de ontwikkeling van een webapplicatie plaatsen. Meestal wordt deze gebouwd op basis van een meerlaagse architectuur, zoals de volgende:

  • de laag [Spring Security] verleent alleen geautoriseerde gebruikers toegang tot de laag [web].

19.2. Een tutorial over Spring Security

We gaan opnieuw een Spring-handleiding importeren door de onderstaande stappen 1 tot en met 3 te volgen:

  

Het project bestaat uit de volgende onderdelen:

  • in de map [templates] bevinden zich de pagina's HTML van het project;
  • [Application]: is de uitvoerbare klasse van het project;
  • [MvcConfig]: is de configuratieklasse van Spring MVC;
  • [WebSecurityConfig]: is de configuratieklasse voor Spring Security;

19.2.1. Maven-configuratie

Het project [3] is een Maven-project. Laten we het bestand [pom.xml] bekijken om de afhankelijkheden te zien:


<?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>
  • regels 10-14: het project is een Spring Boot-project;
  • regels 17-20: afhankelijkheid van het framework [Thymeleaf];
  • regels 22-25: afhankelijkheid van het Spring Security-framework;

19.2.2. De Thymeleaf-weergaven

  

De weergave [home.html] ziet er als volgt uit:

  

<!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>
  • regel 12: het attribuut [th:href="@{/hello}"] genereert het attribuut [href] van de tag <a>. De waarde [@{/hello}] genereert het pad [<context>/hello], waarbij [context] de context van de webapplicatie is;

De gegenereerde code HTML is als volgt:


<!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>

De weergave [hello.html] is als volgt:

  

<!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>
  • regel 9: Het attribuut [th:inline="text"] genereert de tekst van de tag <h1>. Deze tekst bevat een $-uitdrukking die moet worden geëvalueerd. Het element [[${#httpServletRequest.remoteUser}]] is de waarde van het attribuut [RemoteUser] van de huidige aanvraag HTTP. Dit is de naam van de aangemelde gebruiker;
  • regel 10: een formulier HTML. Het attribuut [th:action="@{/logout}"] genereert het attribuut [action] van de tag [form]. De waarde [@{/logout}] genereert het pad [<context>/logout], waarbij [context] de context van de webapplicatie is;

De gegenereerde code HTML is als volgt:


<!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>
  • regel 8: de vertaling van Hello [[${#httpServletRequest.remoteUser}]]!;
  • regel 9: de vertaling van @{/logout};
  • regel 11: een verborgen veld met de naam (attribuut name) _csrf;

De laatste weergave [login.html] is als volgt:

  

<!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>
  • regel 9: het attribuut [th:if="${param.error}"] zorgt ervoor dat de tag <div> alleen wordt gegenereerd als de URL die de inlogpagina weergeeft, de parameter [error] (http://context/login?error) bevat;
  • regel 10: het attribuut [th:if="${param.logout}"] zorgt ervoor dat de tag <div> alleen wordt gegenereerd als de URL, die de inlogpagina weergeeft, de parameter [logout] bevat (http://context/login?logout);
  • regels 11-23: een formulier HTML;
  • regel 11: het formulier wordt verzonden naar de URL [<context>/login], waarbij <context> de context van de webapplicatie is;
  • regel 13: een invoerveld met de naam [username];
  • regel 17: een invoerveld met de naam [password];

De gegenereerde code HTML is als volgt:


<!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>

Op regel 28 is te zien dat Thymeleaf een verborgen veld met de naam [_csrf] heeft toegevoegd.

19.2.3. Spring-configuratie MVC

  

De klasse [MvcConfig] configureert het Spring-framework 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");
    }

}
  • regel 7: de annotatie [@Configuration] maakt van de klasse [MvcConfig] een configuratieklasse;
  • regel 8: de klasse [MvcConfig] breidt de klasse [WebMvcConfigurerAdapter] uit om bepaalde methoden ervan opnieuw te definiëren;
  • regel 10: herdefinitie van een methode van de bovenliggende klasse;
  • regels 11-16: met de methode [addViewControllers] kunnen URL worden gekoppeld aan weergaven HTML. De volgende koppelingen worden hier gemaakt:
URL
weergave
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

De extensie [html] en de map [templates] zijn de standaardwaarden die door Thymeleaf worden gebruikt. Deze kunnen via de configuratie worden gewijzigd. De map [templates] moet zich in de root van het classpath van het project bevinden:

Boven [1] zijn de mappen [java] en [resources] beide bronmappen (source folders). Dit betekent dat hun inhoud zich in de root van het classpath van het project bevindt. Dus in [2] bevinden de mappen [hello] en [templates] zich in de root van het classpath.

19.2.4. Configuratie van Spring Security

  

De klasse [WebSecurityConfig] configureert het Spring Security-framework:


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");
    }
}
  • regel 9: de annotatie [@Configuration] maakt van de klasse [WebSecurityConfig] een configuratieklasse;
  • regel 10: de annotatie [@EnableWebSecurity] maakt van de klasse [WebSecurityConfig] een configuratieklasse van Spring Security;
  • regel 11: de klasse [WebSecurity] breidt de klasse [WebSecurityConfigurerAdapter] uit om bepaalde methoden ervan opnieuw te definiëren;
  • regel 12: herdefinitie van een methode van de bovenliggende klasse;
  • regels 13-16: de methode [configure(HttpSecurity http)] wordt opnieuw gedefinieerd om de toegangsrechten voor de verschillende URL van de applicatie vast te leggen;
  • regel 14: met de methode [http.authorizeRequests()] kunnen URL'en aan toegangsrechten worden gekoppeld. Hierin worden de volgende koppelingen gemaakt:
URL
regel
code
/, /home
toegang zonder authenticatie

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
alleen toegang na authenticatie
http.anyRequest().authenticated();
  • regel 15: definieert de authenticatiemethode. De authenticatie gebeurt via een formulier URL [/login] dat voor iedereen toegankelijk is [http.formLogin().loginPage("/login").permitAll()]. Het uitloggen (logout) is eveneens voor iedereen toegankelijk;
  • regels 19-21: herdefiniëren de methode [configure(AuthenticationManagerBuilder auth)] die de gebruikers beheert;
  • regel 20: de authenticatie vindt plaats met ‘vast’ gedefinieerde gebruikers [auth.inMemoryAuthentication()]. Een gebruiker wordt hier gedefinieerd met de login [user], het wachtwoord [password] en de rol [USER]. Aan gebruikers met dezelfde rol kunnen dezelfde rechten worden toegekend;

19.2.5. Uitvoerbare klasse

  

De klasse [Application] is als volgt:


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);
    }

}
  • regel 8: de annotatie [@EnableAutoConfiguration] vraagt Spring Boot (regel 3) om de configuratie uit te voeren die de ontwikkelaar niet expliciet heeft uitgevoerd;
  • regel 9: maakt van de klasse [Application] een Spring-configuratieklasse;
  • regel 10: vraagt om de map van de klasse [Application] te scannen op zoek naar Spring-componenten. De twee klassen [MvcConfig] en [WebSecurityConfig] worden op deze manier gedetecteerd omdat ze de annotatie [@Configuration] hebben;
  • regel 13: de methode [main] van de uitvoerbare klasse;
  • regel 14: de statische methode [SpringApplication.run] wordt uitgevoerd met de configuratieklasse [Application] als parameter. We zijn dit proces al eerder tegengekomen en weten dat de Tomcat-server die in de Maven-afhankelijkheden van het project is opgenomen, zal worden gestart en dat het project daarop zal worden geïmplementeerd. We hebben gezien dat vier URL-bestanden werden beheerd door [/, /home, /login, /hello] en dat sommige werden beschermd door toegangsrechten.

19.2.6. Testen van de applicatie

Laten we beginnen met het opvragen van de URL [/], een van de vier geaccepteerde URL-bestanden. Deze is gekoppeld aan de weergave [/templates/home.html]:

 

De aangevraagde URL [/] is voor iedereen toegankelijk. Daarom hebben we deze verkregen. De link [here] is als volgt:

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

De URL [/hello] wordt opgevraagd wanneer je op de link klikt. Deze is beveiligd:

URL
regel
code
/, /home
toegang zonder authenticatie

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
alleen toegang na authenticatie
http.anyRequest().authenticated();

Je moet geauthenticeerd zijn om deze te verkrijgen. Spring Security zal de browser van de klant dan doorverwijzen naar de authenticatiepagina. Volgens de getoonde configuratie is dit de pagina URL [/login]. Deze is voor iedereen toegankelijk:


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

We krijgen dus [1]:

De broncode van de verkregen pagina is als volgt:

<!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>
  • op regel 7 verschijnt een verborgen veld dat niet in de oorspronkelijke pagina [login.html] voorkomt. Dit is door Thymeleaf toegevoegd. Deze code, genaamd CSRF (Cross Site Request Forgery), is bedoeld om een beveiligingslek te verhelpen. Dit token moet samen met de authenticatie naar Spring Security worden teruggestuurd, zodat deze wordt geaccepteerd;

We herinneren ons dat alleen de gebruiker user/password door Spring Security wordt herkend. Als we iets anders invoeren in [2], krijgen we dezelfde pagina te zien met een foutmelding in [3]. Spring Security heeft de browser omgeleid naar de pagina URL [http://localhost:8080/login?error]. De aanwezigheid van de parameter [error] heeft ervoor gezorgd dat de tag werd weergegeven:


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

Laten we nu de verwachte waarden voor user/password [4] invoeren:

  • in [4], we loggen in;
  • in [5] leidt Spring Security ons om naar de URL [/hello], omdat dit de URL is die we opvroegen toen we werden doorgestuurd naar de inlogpagina. De identiteit van de gebruiker werd weergegeven in de volgende regel van [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

De pagina [5] toont het volgende formulier:


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

Wanneer u op de knop [Sign Out] klikt, wordt er een POST uitgevoerd op de URL [/logout]. Deze is, net als de URL en [/login], voor iedereen toegankelijk:


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

In onze associatie URL / views hebben we niets gedefinieerd voor de URL en [/logout]. Wat gaat er gebeuren? Laten we het eens proberen:

  • in [6] klikken we op de knop [Sign Out];
  • In [7] zien we dat we zijn doorgestuurd naar URL en [http://localhost:8080/login?logout]. Deze omleiding is door Spring Security aangevraagd. Door de aanwezigheid van de parameter [logout] in URL werd de volgende regel in de weergave getoond:

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

19.2.7. Conclusie

In het vorige voorbeeld hadden we eerst de webapplicatie kunnen schrijven en deze daarna kunnen beveiligen. Spring Security is niet ingrijpend. Het is mogelijk om de beveiliging van een reeds geschreven webapplicatie in te stellen. Daarnaast hebben we het volgende ontdekt:

  • het is mogelijk om een authenticatiepagina te definiëren;
  • de authenticatie moet vergezeld gaan van het door Spring Security verstrekte token CSRF;
  • als de authenticatie mislukt, wordt men doorgestuurd naar de authenticatiepagina, met bovendien een parameter ‘error’ in het token URL;
  • als de authenticatie slaagt, wordt men doorgestuurd naar de pagina die werd opgevraagd op het moment dat de authenticatie plaatsvond. Als de authenticatiepagina rechtstreeks wordt opgevraagd zonder tussenpagina, dan leidt Spring Security ons om naar de URL [/] (dit geval is niet behandeld);
  • je meldt je af door de URL [/logout] op te vragen met een POST. Spring Security leidt ons vervolgens door naar de authenticatiepagina met de parameter logout in de URL;

Al deze conclusies zijn gebaseerd op het standaardgedrag van Spring Security. Dit gedrag kan via de configuratie worden gewijzigd door bepaalde methoden van de klasse [WebSecurityConfigurerAdapter] opnieuw te definiëren.

De vorige tutorial zal ons verderop weinig helpen. We gaan namelijk gebruikmaken van:

  • een database om gebruikers, hun wachtwoorden en hun rollen op te slaan;
  • authenticatie via headers HTTP;

Er zijn vrij weinig tutorials te vinden voor wat we hier willen doen. De oplossing die wordt voorgesteld, is een samenvoeging van code die hier en daar is gevonden.