Dernière modification : 01/01/2021

Spring Boot - Utilisation de Spring security et JPA

Dans cet article nous allons voir comment créer une application utilisant Spring Boot et Spring security avec JPA. Nous utiliserons une base de données PostgresSQL pour la persistence des données. Les technologies utilisées sont les suivantes :

  • Spring Boot 2
  • Spring security
  • JPA
  • Thymeleaf
  • Base de données PostgresSQL
  • Java 11

1. Installation

Afin de préparer l'installation facilement et rapidement, utiliser le générateur de projet disponible à cet adresse : https://start.spring.io . Ajouter Spring web, JPA, thymeleaf , spring security et la base de données postgresql afin de pouvoir les utiliser dans notre application.

On remarquera que dans le fichier pom.xml, le noeud parent, automatiquement généré est le suivant :


	org.springframework.boot
	spring-boot-starter-parent
	2.4.1
	

Les dépendances automatiquement générées sont les suivantes :


	org.springframework.boot
	spring-boot-starter-data-jpa


	org.springframework.boot
	spring-boot-starter-security


	org.springframework.boot
	spring-boot-starter-thymeleaf


	org.springframework.boot
	spring-boot-starter-web


	org.thymeleaf.extras
	thymeleaf-extras-springsecurity5


	org.postgresql
	postgresql
	runtime


	org.springframework.boot
	spring-boot-starter-test
	test


	org.springframework.security
	spring-security-test
	test

2. Définition des entités JPA

Afin de pouvoir définir la structure des tables dans la base de données, nous allons déclarer les entités JPA. Créer un package model où sera stocké les définitions des tables.

La classe User ci-dessous est une définition de la table user dans la base de données. Elle définit un utilisateur.

package com.laulem.springboot.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;

@Entity
@Table(name = "user", schema = "public")
public class User implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "id")
	private Long id;

	@Column(name = "username", length = 65)
	private String username;

	@Column(name = "password", length = 64)
	private String password;

	@OneToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "role_id")
	private Role role;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public Role getRole() {
		return role;
	}

	public void setRole(Role role) {
		this.role = role;
	}
}

La classe Role ci-dessous est une définition de la table role dans la base de données. Elle définit les rôles possibles et affectables à un utilisateur.

package com.laulem.springboot.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "role", schema = "public")
public class Role implements Serializable {
 
	private static final long serialVersionUID = 1L;
 
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "id")
	private Long id;
 
	@Column(name = "role_name", length = 65)
	private String roleName;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getRoleName() {
		return roleName;
	}

	public void setRoleName(String roleName) {
		this.roleName = roleName;
	}
}

3. Création du repository

Afin de pouvoir récupérer des informations issues de la base de données, nous allons utiliser JpaRepository, qui nous fera gagner du temps, en implémentant directement plusieurs méthodes de récupération de données. Créer le package repository.

Dans le package repository, insérer la classe UserRepository ci-dessous :

package com.laulem.springboot.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.laulem.springboot.model.User;

public interface UserRepository extends JpaRepository {
    User findByUsername(String username);
}

La méthode findByUsername permettra de récupérer l'utilisateur par son username.

Remarque : Pour plus d'informations à propos de JpaRepository, la documentation est disponible à cette adresse : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference

4. Création de la couche service

Nous allons maintenant créer la couche service. Cette couche accédera au repository, l'isolant du reste de l'application.

Créer le package service où sera entreposé les services.

Dans ce dernier package, insérer l'interface IUserService suivante :

package com.laulem.springboot.service;

import com.laulem.springboot.model.User;

public interface IUserService {
    User findByUsername(String username);
}

Insérer également le service UserService suivant :

package com.laulem.springboot.service;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.laulem.springboot.model.User;
import com.laulem.springboot.repository.UserRepository;

@Transactional
@Service("userService")
public class UserService implements IUserService {
	@Autowired
	private UserRepository userRepository;

	@Override
	public User findByUsername(String username) {
		return userRepository.findByUsername(username);
	}
}

5. Sécurisation de l'application

Afin de sécuriser l'application, nous devons implémenter l'interface UserDetailsService. En l'implémentant, nous allons récupérer l'utilisateur de la base de données à l'aide de son username.

Encore dans le package service, créer la classe CustomUserDetailsService comme suit :

package com.laulem.springboot.service;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 com.laulem.springboot.model.Role;
import com.laulem.springboot.model.User;

@Service("customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService {

	@Autowired
	private UserService userService;

	@Override
	@Transactional
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = Optional.ofNullable(userService.findByUsername(username))
				.orElseThrow(() -> new UsernameNotFoundException("User " + username + " not found"));
		return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
				getGrantedAuthorities(user));
	}

	private List getGrantedAuthorities(User user) {
		Role role = Optional.ofNullable(user.getRole()).orElse(new Role());
		return Arrays.asList(new SimpleGrantedAuthority(role.getRoleName()));
	}
}

Dans le package config à créer, nous allons définir la politique de sécurité de l'application. Premièrement nous acceptons l'accès à tous les utilisateurs sur la page login, mais uniquement ceux qui ont réussi à se connecter sur la page welcome :

package com.laulem.springboot.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	@Qualifier("customUserDetailsService")
	private UserDetailsService customUserDetailsService;
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
	    http.csrf().disable()
		.authorizeRequests().antMatchers("/").permitAll()
		.anyRequest().authenticated()
		.and().formLogin().loginPage("/login").defaultSuccessUrl("/welcome").failureUrl("/login?error=true").permitAll()
		.and().logout().deleteCookies("JSESSIONID").logoutUrl("/logout").logoutSuccessUrl("/login"); 
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
		authManagerBuilder.userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder());
	}
	
	@Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
	
	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
}

6. Définition de la vue et du controller

Nous allons maintenant définir la vue et le controller. Nous avons au total deux pages : la page welcome et la page de connexion.

Créer un package controller et insérer la classe suivante :

package com.laulem.springboot.controller;

import java.util.Date;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class BasicController {
	@GetMapping("/")
	public String homePage(Model model) {
		return "login";
	}

	@GetMapping("/login")
	public String loginPage(Model model) {
		return "login";
	}
	
	@GetMapping("/welcome")
	public String welcomePage(Model model) {
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		model.addAttribute("username", auth.getPrincipal());

		return "welcome";
	}
}

Dans le dossier src/main/ressources/templates, créer le fichier login.html et insérer le contenu suivant :





	Login
	
  	
  	




	
	


Cette page ci-dessus nous servira de page de connexion.

La page welcome.html ci-dessous nous servira de page d'accueil :





Welcome


	

Welcome !


7. Configuration de la connexion à la base de données

Sans cette configuration, l'application ne saurait pas rechercher dans la base de données PostgresSQL. Pour ce faire, dans le fichier application.properties, ajouter (et compléter) la configuration suivante :

## default connection pool
spring.datasource.hikari.connectionTimeout=20000
spring.datasource.hikari.maximumPoolSize=5

## PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/NOM_BDD_A_COMPLETER
spring.datasource.username=USERNAME_A_COMPLETER
spring.datasource.password=PASSWORD_A_COMPLETER

# Comment this in production
spring.jpa.hibernate.ddl-auto=update

8. Lancement

Exécuter une première fois l'application afin que les tables soient créées dans la base de données, puis insérer les données suivantes :

INSERT INTO public.role (id, role_name) VALUES (1, 'ROLE_ADMIN');
INSERT INTO public.role (id, role_name) VALUES (2, 'ROLE_USER');

INSERT INTO public.user (id, password, username, role_id) VALUES (1, '$2y$12$zRcUApFsej/Ph3il3/4dN.LSDKxDFMluMorJicMwP0MRtkDhhgQJa', 'admin', 1);
INSERT INTO public.user (id, password, username, role_id) VALUES (2, '$2y$12$L3vgt7kgKMK8sTszQUkjP.zOnC2PcPK3R9znVR0UoDKl8lb9wPvGq', 'user', 2);

Vous pouvez ensuite vous identifier via les deux utilisateurs suivants :

  • username : user, password : user
  • username : admin, password : admin

Remarque : Les mots de passes sont des hash BCrypt. Vous trouverez beaucoup de convertisseur en ligne de mots de passes en hash BCrypt afin d'obtenir un mot de passe plus sécurisé.