L'authentification de la plateforme e-commerce utilise JWT (JSON Web Tokens) signés avec un chiffrement asymétrique RSA-2048. Voici le flux de sécurité global :
┌──────────────┐ ┌─────────────────────┐ ┌──────────────┐
│ Client │ │ Membership Service │ │ │
│ │ │ (ms-membership) │ │ BD: Users │
└──────────────┘ └─────────────────────┘ └──────────────┘
│ │ │
│ POST /api/v1/auth/login │ │
│ {email, password} │ │
├────────────────────────────────────>│ │
│ │ Chercher utilisateur par email │
│ ├────────────────────────────────────>│
│ │ │
│ │<─────────────────────────────────────┤
│ │ User{id, email, active} │
│ │ │
│ │ Signer JWT avec clé privée RSA │
│ │ Payload: userId, email, roles, exp │
│ │ │
│<──────────────────────────────────────┤ │
│ {token, expiresIn: 3600} │ │
│ │ │
┌──────────────┐ ┌─────────────────────┐
│ Client │ │ Product Service │
│ │ │ (ms-product) │
└──────────────┘ └─────────────────────┘
│ │
│ GET /api/v1/products │
│ Authorization: Bearer <JWT> │
├────────────────────────────────────>│
│ │ Extraire token du header
│ │ Valider signature avec clé publique RSA
│ │ Vérifier expiration (exp claim)
│ │ Extraire userId, roles
│ │
│<──────────────────────────────────────┤
│ [Produits] │
│ │
- Clé privée : Gardée secrète par ms-membership. Utilisée pour signer les tokens.
- Clé publique : Partagée avec tous les services. Utilisée pour vérifier la signature des tokens.
- Avantage : Pas besoin de partager la clé privée aux autres services.
Vous devez générer une paire de clés RSA 2048 bits.
- Linux/Mac : Généralement préinstallé. Vérifiez :
openssl version - Windows : Téléchargez depuis https://slproweb.com/products/Win32OpenSSL.html (version Light)
Ouvrez un terminal/PowerShell et exécutez :
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048Exemple de sortie :
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCB...
... (clé encodée en Base64) ...
-----END PRIVATE KEY-----
openssl rsa -pubout -in private_key.pem -out public_key.pemExemple de sortie :
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BA...
... (clé encodée en Base64) ...
-----END PUBLIC KEY-----
ms-membership/
├── src/
│ └── main/
│ └── resources/
│ └── keys/
│ ├── private_key.pem ← Garder SECRET !
│ ├── public_key.pem ← Optionnel (pour info)
│ ├── .gitignore ← ignore private_key.pem
│ └── README_KEYS.md
Action :
- Copiez le fichier
private_key.pemgénérés dansms-membership/src/main/resources/keys/ - NE PAS committer cette clé à Git — elle est déjà ignorée via
.gitignore
Note : Le fichier private_key.pem n'est pas stocké dans le dépôt par sécurité. Pour les tests locaux générez la paire RSA et placez private_key.pem dans ms-membership/src/main/resources/keys/ puis copiez public_key.pem vers ms-product et ms-order.
ms-product/
├── src/
│ └── main/
│ └── resources/
│ └── keys/
│ ├── public_key.pem ← Copiez depuis ms-membership
│ └── README_KEYS.md
ms-order/
├── src/
│ └── main/
│ └── resources/
│ └── keys/
│ ├── public_key.pem ← Copiez depuis ms-membership
│ └── README_KEYS.md
Action :
- Copiez le fichier
public_key.pemdansms-product/src/main/resources/keys/ - Copiez le fichier
public_key.pemdansms-order/src/main/resources/keys/ - Vous pouvez committer la clé publique à Git (elle est non-sensible)
Un JWT contient 3 parties séparées par des points :
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidXNlcklkIjoxLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiUk9MRV9VU0VSIl0sImV4cCI6MTY4NzcyNzIwMCwiaWF0IjoxNjg3NzIzNjAwfQ.signature...
│ │ │
└─────────────────────────────────────────────────────────────────────┘────────────────────────────────────────────────────────┘
HEADER.PAYLOAD SIGNATURE
{
"alg": "RS256",
"typ": "JWT"
}alg: Algorithme de signature (RS256 = RSA avec SHA256)typ: Type de token
{
"sub": "1",
"userId": 1,
"email": "alice@example.com",
"roles": ["ROLE_USER"],
"iat": 1687723600,
"exp": 1687727200
}sub: Subject (identité utilisateur)userId: ID utilisateur (custom claim)email: Email utilisateur (custom claim)roles: Rôles utilisateur (custom claim, liste)iat: Issued At (timestamp Unix création)exp: Expiration (timestamp Unix, 1 heure après iat)
HMACSHA256(
base64url(header) + "." + base64url(payload),
privateKey
)
Générée avec la clé privée RSA du Membership service. Elle garantit que le token n'a pas été modifié.
- Expiration : 3600 secondes (1 heure)
- Les services vérifient
expet rejettent les tokens expirés avec un 403 Forbidden
Endpoint : POST /api/v1/auth/login
Requête :
{
"email": "alice.durand@example.com",
"password": "password123"
}Réponse (200 OK) :
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600
}Erreurs :
400 Bad Request: Données manquantes ou invalides401 Unauthorized: Email/password invalide403 Forbidden: Utilisateur désactivé
Code : AuthController.java
Endpoint Example : GET /api/v1/products
Requête :
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:8082/api/v1/productsValidations dans JwtAuthFilter :
- Extraire le token du header
Authorization: Bearer <token> - Parser le JWT (extraire header, payload, signature)
- Vérifier la signature avec la clé publique RSA
- Vérifier l'expiration (claim
exp) - Extraire les claims (userId, email, roles)
- Créer un authentication Spring Security avec ces claims
- Passer au prochain filtre (chaîne de sécurité)
Réponses :
200 OK: Token valide, accès accordé401 Unauthorized: Token manquant, invalide ou signature échouée403 Forbidden: Token expiré
Code :
- Product: JwtAuthFilter.java
- Order: JwtAuthFilter.java
| Code | Sens | Exemple |
|---|---|---|
| 401 Unauthorized | Token manquant, invalide, ou signature incorrecte | Pas de header Authorization, clé publique ne correspond pas à la signature |
| 403 Forbidden | Token expiré | exp claim antérieur à la date actuelle |
curl http://localhost:8082/api/v1/productsResponse: 401 Unauthorized
Missing or invalid Authorization header
curl -H "Authorization: Bearer <expired_token>" \
http://localhost:8082/api/v1/productsResponse: 403 Forbidden
Token expired
curl -H "Authorization: Bearer <token_with_modified_payload>" \
http://localhost:8082/api/v1/productsResponse: 401 Unauthorized
Invalid token signature
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
JwtAuthFilter jwtFilter = new JwtAuthFilter();
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll() // Health check sans auth
.anyRequest().authenticated() // Tous les autres endpoints nécessitent un token
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}Points clés :
- .csrf().disable() : Les JWTs ne sont pas vulnérables aux CSRF (tokens stateless)
- STATELESS : Pas de sessions ; chaque requête contient son token
- /actuator/health : Autorisé sans token (pour les health checks des conteneurs Docker)
- Tous les autres endpoints : Require JWT valide
- JwtAuthFilter : Injecté avant le filtre d'authentification standard
Quand le service Order appelle le service Product ou User :
// Exemple : Order Service appelle Product Service pour vérifier un produit
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public void createOrder(OrderRequest request, String jwtToken) {
// Propager le JWT dans le header
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + jwtToken);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<ProductDTO> response = restTemplate.exchange(
"http://localhost:8082/api/v1/products/" + request.getProductId(),
HttpMethod.GET,
entity,
ProductDTO.class
);
// Gérer les erreurs
if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new SecurityException("Token invalide au service Product");
}
if (response.getStatusCode() == HttpStatus.FORBIDDEN) {
throw new SecurityException("Token expiré au service Product");
}
}
}┌──────────┐ ┌─────────────┐ ┌────────────────┐
│ Client │ │ Membership │ │ Product Service│
└──────────┘ └─────────────┘ └────────────────┘
│ │ │
│ 1. POST /auth/login │ │
│ (email, password) │ │
├────────────────────────────>│ │
│ │ │
│ 2. Query DB (User) │
│ Vérifier active=true │
│ │ │
│ 3. Signer JWT │ │
│ RS256(header.payload) │ │
│<────────────────────────────── │
│ {token, expiresIn} │ │
│ │ │
│ 4. GET /products │ │
│ Authorization: Bearer TOKEN │ │
├─────────────────────────────────────────────────────────>│
│ │ │
│ │ 5. JwtAuthFilter │
│ │ - Extraire token │
│ │ - Parser JWT │
│ │ - Verify RSA sign │
│ │ - Check expiration │
│ │ │
│ │<──────────────────────────┐│
│ │ Si valide: authenticate │
│ │ Si invalide/expiré: 401/403
│ │ │
│<─────────────────────────────────────────────────────────┤
│ │ [Products] │
│ │ │
server:
port: 8081
servlet:
context-path: /
spring:
application:
name: ms-membership
jpa:
hibernate:
ddl-auto: update
h2:
console:
enabled: true
jwt:
secret: unused-with-asymmetric-keys
expiration: 3600000 # 1 hour in msserver:
port: 8082
servlet:
context-path: /
spring:
application:
name: ms-product
security:
filter:
order: 5
jwt:
public-key-path: classpath:keys/public_key.pemserver:
port: 8083
servlet:
context-path: /
spring:
application:
name: ms-order
security:
filter:
order: 5
jwt:
public-key-path: classpath:keys/public_key.pemPOST http://localhost:8081/api/v1/auth/login
Content-Type: application/json
{
"email": "alice.durand@example.com",
"password": "password123"
}
Réponse (200 OK) :
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600
}GET http://localhost:8082/api/v1/products
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Réponse (200 OK) :
[
{ "id": 1, "name": "Laptop", "price": 999.99 },
...
]GET http://localhost:8082/api/v1/products
Réponse (401 Unauthorized) :
Missing or invalid Authorization header
Attendre que le token expire (> 3600 sec) ou modifier le exp claim manuellement.
GET http://localhost:8082/api/v1/products
Authorization: Bearer <expired_token>
Réponse (403 Forbidden) :
Token expired
Cause : Fichier private_key.pem manquant dans ms-membership/src/main/resources/keys/
Solution :
- Générez les clés avec OpenSSL (voir section 1)
- Placez
private_key.pemdans le bon chemin - Restartez ms-membership
Cause : La clé publique dans ms-product/ms-order ne correspond pas à la clé privée de ms-membership
Solution :
- Vérifiez que vous avez copié la même
public_key.pemdepuis ms-membership - Régénérez les clés si nécessaire
- Assurez-vous que
public_key.pemdans ms-product et ms-order est identique
Cause : L'horloge du serveur est incorrecte
Solution :
- Synchronisez l'horloge système
- Vérifiez que
iatetexpsont correctement définis - Pour les tests, augmentez temporairement
expiresInSecondsdansJwtService.generateToken()
- ✅ Clé privée générée avec RSA 2048 bits
- ✅ Clé privée stockée dans
.gitignore(jamais committer) - ✅ Clé publique copiée dans ms-product et ms-order
- ✅ JwtAuthFilter implémenté dans tous les services consommateurs
- ✅ SecurityConfig enregistre JwtAuthFilter avant UsernamePasswordAuthenticationFilter
- ✅ /actuator/health permettra sans authentication (pour Docker health checks)
- ✅ Tous les autres endpoints nécessitent JWT valide
- ✅ Tokens expirent après 1 heure
- ✅ Tests Postman valident les scénarios 401 et 403