Sécuriser l'application
Cours · Spring Boot : écrire le serveur (école d'ingénieur), chapitre 8 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
Essayez, sur la Librairie du chapitre précédent :
$ curl -i -X DELETE http://localhost:8000/api/livres/9782956300014
La réponse est un 204 No Content : n'importe qui, depuis n'importe où, vient de retirer Programmation Java du catalogue. Il en va de même pour modifier un prix, ou pour lire la liste des commandes, avec les noms et les adresses des clients. Une API ouverte à tous est une API dont chaque fonction peut être détournée.
Ce chapitre ferme ces portes avec Spring Security. Les visiteurs continuent de consulter le catalogue et de passer commande sans compte : c'est ce que fait l'application Vue. Mais modifier le catalogue est réservé à l'administrateur, et la liste des commandes au personnel. Spring Security a la réputation d'être difficile ; il l'est surtout quand on le configure sans savoir ce qu'il fait. Nous regardons donc d'abord ce qu'il fait par défaut, et pourquoi, avant d'écrire nos propres règles.
Objectifs du chapitre
- Distinguer authentification et autorisation, et les réponses 401 et 403.
- Comprendre la chaîne de filtres de Spring Security et sa configuration par défaut.
- Ranger des mots de passe de façon sûre, avec BCrypt.
- Écrire des règles d'accès par adresse, par méthode HTTP et par rôle, dans le bon ordre.
- Expliquer l'attaque CSRF et décider en connaissance de cause de la protection à appliquer.
- Protéger la Librairie, avec des comptes rangés en base, et adapter ses tests.
Les démonstrations des sections 8.2 à 8.5 sont dans le projet code/ch8/demo-securite : une petite API, sans base, dont la configuration de sécurité change selon le profil choisi au lancement. La Librairie protégée est dans code/ch8/librairie.
8.1 Qui êtes-vous, et qu'avez-vous le droit de faire ?
L'authentification établit qui fait la requête : un nom et une preuve (un mot de passe, un jeton). L'autorisation décide si cette personne, une fois connue, a le droit de faire ce qu'elle demande. Les deux échecs ont chacun leur statut HTTP :
- 401 Unauthorized : l'identité manque ou n'est pas prouvée (pas de mot de passe, mauvais mot de passe). Malgré son nom, c'est un échec d'authentification ; la réponse indique, par l'en-tête
WWW-Authenticate, comment s'identifier. - 403 Forbidden : l'identité est établie, mais elle n'a pas le droit. Recommencer avec le même compte ne servira à rien.
Les droits se donnent rarement personne par personne : on attribue à chaque compte un ou plusieurs rôles (ADMIN, COMPTABLE), et les règles portent sur les rôles. Pour la Librairie :
| requête | qui | pourquoi |
|---|---|---|
| lire le catalogue, passer commande | tout le monde | l'application Vue |
| lire la liste des commandes | ADMIN, COMPTABLE | la comptabilité |
| créer, modifier, supprimer un livre | ADMIN | le libraire |
| console H2 | ADMIN | le développement |
| tout le reste | personne | fermé par défaut |
8.2 Ce que fait Spring Security par défaut
8.2.1 Tout est fermé
Ajoutons au projet de démonstration le seul starter spring-boot-starter-security, sans aucune configuration. Au démarrage, le journal contient deux lignes nouvelles :
Using generated security password: 77b0f2f7-a37f-4ef8-a43c-728efb34a485
This generated password is for development use only. Your security configuration must be updated before running your application in production.
Et l'API, qui répondait à tous, ne répond plus à personne :
$ curl -i http://localhost:8080/api/livres
HTTP/1.1 401
Set-Cookie: JSESSIONID=320A91EE967762FAF396069D5B155FDB; Path=/; HttpOnly
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
Content-Length: 0
Spring Boot a fermé toutes les adresses et créé un compte unique, user, avec un mot de passe aléatoire, différent à chaque démarrage. Le 401 porte l'en-tête WWW-Authenticate: Basic : le serveur accepte l'authentification HTTP Basic, où le client envoie le nom et le mot de passe dans l'en-tête Authorization. curl -u le fait :
$ curl -i -u user:77b0f2f7-a37f-4ef8-a43c-728efb34a485 http://localhost:8080/api/livres
HTTP/1.1 200
Content-Type: application/json
Content-Length: 40
["Programmation Java","Le Petit Prince"]
Un navigateur, lui, reçoit autre chose (on le simule en demandant du HTML) :
$ curl -i -H 'Accept: text/html' http://localhost:8080/api/livres
HTTP/1.1 302
Set-Cookie: JSESSIONID=99C54B36E4CDB870D70EBDAD105F2EC9; Path=/; HttpOnly
Location: http://localhost:8080/login
Content-Length: 0
une redirection vers une page de connexion, /login, générée par Spring Security. La configuration par défaut sert les deux mondes.
Dès que Spring Security est présent, toute requête exige une authentification ; Spring Boot crée un compte user au mot de passe aléatoire (affiché dans le journal), active HTTP Basic et un formulaire de connexion, la protection CSRF et des en-têtes de sécurité. On part donc d'une application fermée, et c'est à nous d'ouvrir ce qui doit l'être. Le compte user disparaît dès qu'on déclare ses propres comptes.
8.2.2 Une chaîne de filtres
Comment Spring Security intercepte-t-il les requêtes, avant même nos contrôleurs ? Par des filtres : des objets placés devant la DispatcherServlet, que chaque requête traverse dans l'ordre. Avec le journal de DefaultSecurityFilterChain au niveau DEBUG, la liste s'affiche au démarrage :
Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultResourcesFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter
Quatre d'entre eux font l'essentiel : CsrfFilter vérifie le jeton CSRF des requêtes qui modifient (section 8.5) ; BasicAuthenticationFilter lit l'en-tête Authorization et vérifie le mot de passe ; AuthorizationFilter applique les règles d'accès ; et ExceptionTranslationFilter, placé juste avant, transforme les refus en réponses : 401 pour un anonyme, 403 pour une personne connue.
8.3 Des mots de passe qu'on ne peut pas lire
Avant d'écrire nos comptes, une règle absolue : un serveur ne range jamais un mot de passe en clair, ni même chiffré (ce qui se chiffre se déchiffre). Il range une empreinte, calculée par une fonction à sens unique, et, à la connexion, recalcule l'empreinte du mot de passe proposé pour la comparer.
Une fonction de hachage de mot de passe (BCrypt, Argon2, scrypt) transforme un mot de passe en une empreinte dont on ne peut pas remonter au mot de passe. Elle y ajoute un sel, une valeur aléatoire propre à chaque empreinte, et elle est volontairement lente : vérifier un mot de passe prend une fraction de seconde, ce qui ne gêne pas un utilisateur mais rend très coûteux d'essayer des milliards de mots de passe sur une empreinte volée.
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.