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 :
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 :
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/>
</parent>
Les dépendances automatiquement générées sont les suivantes :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
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;
}
}
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, Long> {
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
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);
}
}
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<GrantedAuthority> 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();
}
}
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 :
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<style type="text/css">
form {
width:30%;
margin:auto;
}
p {
font-size: 20;
color: #FF1C19;
}
</style>
</head>
<body>
<div class="container">
<br/><br/>
<form th:action="@{/login}" method="POST" class="form-signin">
<h3 class="form-signin-heading">LOGIN</h3>
<br/>
<div align="center" th:if="${param.error}">
<p>Invalid username or password</p>
</div>
<input type="text" id="username" name="username" th:placeholder="Username" class="form-control" /> <br/>
<input type="password" th:placeholder="Password" id="password" name="password" class="form-control" /> <br />
<button class="btn btn-lg btn-primary btn-block" name="Submit" value="Login" type="Submit" th:text="Login"></button>
</form>
</div>
</body>
</html>
Cette page ci-dessus nous servira de page de connexion.
La page welcome.html ci-dessous nous servira de page d'accueil :
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
<h1>Welcome !</h1>
<br />
<a th:href="@{/logout}">
<button type="button">LOGOUT</button>
</a>
<div align="center" th:if="${username != null}">
<div th:text="${username}" />
</div>
</body>
</html>
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
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 :
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é.
LauLem.com - Conditions Générales d'Utilisation - Informations Légales - Charte relative aux cookies - Charte sur la protection des données personnelles - A propos