Dans le contexte du développement logiciel d'entreprise, il est fréquent de constater que les phases initiales d'un projet ne nécessitent pas une architecture en microservices complexe. Une application monolithique robuste, standardisée et extensible représente souvent le choix optimal.
Une grande partie des ressources disponibles en ligne demeure ancrée dans l'écosystème Spring Boot 2.x voire JDK 8. Leurs configurations s'apparentent souvent à des prototypes pédagogiques. Les composants essentiels à la production tels qu'une authentification RSA+JWT solide, une gestion centralisée des exceptions, la configuration des CORS, un système de double cache Redis ainsi qu'un déploiement Docker normalisé sont rarement intégrés de manière complète et prête à l'emploi.
Pallier cette lacune est l'ojbectif de cet article : assembler une fondation technique éprouvée. L'idée est de disposer d'un socle de production sur lequel il suffit d'adapter le package de base pour démarrer immédiatement le développement fonctionnel.
Prérequis environnementaux
- JDK : version 17 ou supérieure (exigence formelle de Spring Boot 3.x)
- Maven : version 3.8+
- MySQL : version 8.0+
- Redis : version 5.0+
- Nacos : version 2.x (comme centre de configuration, optionnel mais recommandé)
Démarche de mise en œuvre
1. Gestion des dépendances fondamentales
Création d'un projet Maven standard pour Spring Boot. Voici un exemple de fichier pom.xml structuré différemment, avec des versions et commentaires propres.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
</parent>
<properties>
<java.version>17</java.version>
<mbp.version>3.5.8</mbp.version>
<sca.version>2023.0.1.0</sca.version>
<openapi.version>2.6.0</openapi.version>
</properties>
<dependencies>
<!-- Module Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Configuration Nacos (Spring Cloud Alibaba) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${sca.version}</version>
</dependency>
<!-- Découverte Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${sca.version}</version>
</dependency>
<!-- ORM -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mbp.version}</version>
</dependency>
<!-- Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jeton d'authentification -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
<!-- Documentation API -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${openapi.version}</version>
</dependency>
</dependencies>
2. Intégration et configuration de Nacos
Depuis Spring Boot 2.4, la méthode privilégiée pour importer les configurations distantes utilise la directive import, en délaissant le fichier bootstrap.yml.
Fichier de configuration local (application.yml) :
server:
port: 8089
shutdown: graceful
spring:
profiles:
active: dev
application:
name: prod-app-core
cloud:
nacos:
config:
server-addr: ${NACOS_HOST:127.0.0.1:8848}
file-extension: yml
group: MAIN_GROUP
namespace: VOTRE_ID_ESPACE
config:
import:
- nacos:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
Configuration distante Nacos (prod-app-core-dev.yml) :
Créer ce fichier de configuration et y coller les paramètres ci-dessous. Notez l'utilisation d'une section app.global pour stocker des secrets comme la clé RSA.
spring:
datasource:
dynamic:
primary: master
hikari:
maximum-pool-size: 8
datasource:
master:
url: jdbc:mysql://dbhost:3306/prod_db?serverTimezone=Europe/Paris&characterEncoding=utf8mb4
username: prod_user
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: redis-host
port: 6379
lettuce:
pool:
max-active: 50
max-idle: 10
# Configuration applicative globale
app:
global:
# Clé privée RSA (utilisée pour le déchiffrement côté serveur)
rsa-private-key: MIICdwIBADANBgkqhkiG9w0BAQEFAASC...
3. Configurations d'infrastructure essentielles
CORS (Cross-Origin Resource Sharing) :
@Configuration
public class CorsSettings {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}
Pagination avec MyBatis Plus :
@Configuration
@MapperScan("com.example.project.repository")
public class PersistenceConfig {
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(500L);
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
Documentation API (OpenAPI 3) :
@Configuration
public class ApiDocumentationConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("API de production")
.version("1.0")
.description("Documentation des endpoints de l'application"))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
Sérialisation pour Redis :
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
4. Gestion centralisée des exceptions
Un gestionnaire global est crucial pour la stabilité et la cohérence des réponses d'erreur.
@Slf4j
@RestControllerAdvice
public class GlobalErrorAdvisor {
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorDto> handleAuthFailure(AuthenticationException ex) {
log.warn("Échec d'authentification: {}", ex.getMessage());
ErrorDto error = new ErrorDto("AUTH_FAILURE", ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorDto> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
ErrorDto error = new ErrorDto("VALIDATION_ERROR", message);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDto> handleGenericException(Exception ex, WebRequest request) {
log.error("Erreur interne sur {}", request.getDescription(false), ex);
ErrorDto error = new ErrorDto("SERVER_ERROR", "Une erreur interne est survenue.");
return ResponseEntity.internalServerError().body(error);
}
}
5. Chaîne complète d'authentification sécurisée
Le flux sécurisé implémente : chiffrement RSA côté client → déchiffrement côté serveur → vérfiication → génération JWT → mise en cache Redis.
Service d'authentification :
@Service
@Slf4j
public class AuthenticationServiceImpl implements AuthenticationService {
private final UserRepository userRepository;
private final JwtTokenProvider tokenProvider;
private final RsaKeyProperties rsaKeys;
private final RedisTemplate<String, String> redisTemplate;
// Injection via constructeur
public AuthenticationServiceImpl(UserRepository userRepository,
JwtTokenProvider tokenProvider,
RsaKeyProperties rsaKeys,
RedisTemplate<String, String> redisTemplate) {
this.userRepository = userRepository;
this.tokenProvider = tokenProvider;
this.rsaKeys = rsaKeys;
this.redisTemplate = redisTemplate;
}
@Override
public AuthResponse authenticate(EncryptedLoginRequest request) {
try {
// Déchiffrement du payload
String decryptedJson = CryptoUtil.decryptWithPrivateKey(
request.getPayload(),
rsaKeys.getPrivateKey()
);
LoginCredentials creds = new ObjectMapper().readValue(decryptedJson, LoginCredentials.class);
// Vérification des identifiants
User user = userRepository.findByLogin(creds.username())
.orElseThrow(() -> new BadCredentialsException("Identifiants invalides"));
if (!passwordEncoder.matches(creds.password(), user.getPasswordHash())) {
throw new BadCredentialsException("Identifiants invalides");
}
// Création et stockage du jeton
String jwt = tokenProvider.generateToken(user.getId());
String sessionKey = "session:" + jwt;
String userSessionKey = "user_session:" + user.getId();
redisTemplate.opsForValue().set(sessionKey, user.getId(), Duration.ofHours(2));
redisTemplate.opsForValue().set(userSessionKey, jwt, Duration.ofHours(2));
return new AuthResponse(jwt, user.getDisplayName());
} catch (Exception e) {
log.error("Échec du processus d'authentification", e);
throw new AuthenticationServiceException("Échec de l'authentification", e);
}
}
}
Filtre d'interception des requêtes :
@Component
@Order(1)
public class JwtValidationFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private final RedisTemplate<String, String> redisTemplate;
public JwtValidationFilter(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
String jwt = authHeader.substring(BEARER_PREFIX.length());
String userId = redisTemplate.opsForValue().get("session:" + jwt);
if (userId != null) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new UsernamePasswordAuthenticationToken(userId, jwt, Collections.emptyList());
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
// Renouvellement de la session
redisTemplate.expire("session:" + jwt, Duration.ofHours(2));
}
}
filterChain.doFilter(request, response);
}
}
6. Déploiement conteneurisé
Spring Boot 3.x impose l'usage de JDK 17+. Le Dockerfile doit en tenir compte.
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --chown=1001:1001 target/*.jar app.jar
EXPOSE 8080
USER 1001
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", "app.jar"]
Commandes pour construire et exécuter :
# Construction de l'image
docker build -t myapp:1.0.0 .
# Exécution avec injection de variables d'environnement
docker run -d --name app-instance -p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e NACOS_HOST=nacos.example.com:8848 \
-e DB_PASSWORD=secret \
myapp:1.0.0