spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
Juste pour vous donner un peu de contexte, nous avons une merveilleuse introduction, gracieuseté de jwt.io ! Jetons un coup d'oeil :
JSON Web Token (JWT) est une norme ouverte (RFC 7519) qui définit un moyen compact et autonome de transmettre en toute sécurité des informations entre les parties en tant qu'objet JSON. Ces informations peuvent être vérifiées et fiables car elles sont signées numériquement. Les JWT peuvent être signés à l'aide d'un secret (avec l'algorithme HMAC) ou d'une paire de clés publique/privée à l'aide de RSA.
Expliquons plus en détail certains concepts de cette définition.
Compact : en raison de leur taille plus petite, les JWT peuvent être envoyés via une URL, un paramètre POST ou dans un en-tête HTTP. De plus, la taille plus petite signifie que la transmission est rapide.
Autonome : la charge utile contient toutes les informations requises sur l'utilisateur, évitant ainsi d'avoir à interroger la base de données plus d'une fois.
Voici quelques scénarios dans lesquels les jetons Web JSON sont utiles :
Authentification : il s'agit du scénario le plus courant pour l'utilisation de JWT. Une fois l'utilisateur connecté, chaque requête ultérieure inclura le JWT, permettant à l'utilisateur d'accéder aux itinéraires, services et ressources autorisés avec ce jeton. L'authentification unique est une fonctionnalité qui utilise largement JWT de nos jours, en raison de sa faible surcharge et de sa capacité à être facilement utilisée dans différents domaines.
Échange d'informations : les jetons Web JSON sont un bon moyen de transmettre en toute sécurité des informations entre les parties. Étant donné que les JWT peuvent être signés (par exemple, à l'aide de paires de clés publique/privée), vous pouvez être sûr que les expéditeurs sont bien ceux qu'ils prétendent être. De plus, comme la signature est calculée à l'aide de l'en-tête et de la charge utile, vous pouvez également vérifier que le contenu n'a pas été falsifié.
Les jetons Web JSON se composent de trois parties séparées par des points (.) , qui sont :
Par conséquent, un JWT ressemble généralement à ce qui suit.
xxxxx
. yyyyy
. zzzzz
Décomposons les différentes parties.
En-tête
L'en-tête se compose généralement de deux parties : le type du jeton, qui est JWT, et l'algorithme de hachage utilisé, tel que HMAC SHA256 ou RSA.
Par exemple:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
Ensuite, ce JSON est codé en Base64Url pour former la première partie du JWT.
Charge utile
La deuxième partie du jeton est la charge utile, qui contient les revendications. Les revendications sont des déclarations sur une entité (généralement l'utilisateur) et des métadonnées supplémentaires. Il existe trois types de créances : les créances réservées, publiques et privées.
Notez que les noms des revendications ne comportent que trois caractères car JWT est censé être compact.
Revendications publiques : celles-ci peuvent être définies à volonté par ceux qui utilisent les JWT. Mais pour éviter les collisions, ils doivent être définis dans le registre de jetons Web IANA JSON ou être définis comme un URI contenant un espace de noms résistant aux collisions.
Revendications privées : ce sont les revendications personnalisées créées pour partager des informations entre les parties qui acceptent de les utiliser.
Un exemple de charge utile pourrait être :
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
La charge utile est ensuite codée en Base64Url pour former la deuxième partie du jeton Web JSON.
Signature
Pour créer la partie signature, vous devez prendre l'en-tête codé, la charge utile codée, un secret, l'algorithme spécifié dans l'en-tête, et le signer.
Par exemple si vous souhaitez utiliser l'algorithme HMAC SHA256, la signature sera créée de la manière suivante :
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
La signature est utilisée pour vérifier que l'expéditeur du JWT est bien celui qu'il prétend être et pour garantir que le message n'a pas été modifié en cours de route. Rassembler le tout
Le résultat est constitué de trois chaînes Base64 séparées par des points qui peuvent être facilement transmises dans les environnements HTML et HTTP, tout en étant plus compactes par rapport aux normes basées sur XML telles que SAML.
Ce qui suit montre un JWT dont l'en-tête et la charge utile précédents sont codés, et il est signé avec un secret. JWT codé
Lors de l'authentification, lorsque l'utilisateur se connecte avec succès à l'aide de ses informations d'identification, un jeton Web JSON sera renvoyé et doit être enregistré localement (généralement dans un stockage local, mais les cookies peuvent également être utilisés), au lieu de l'approche traditionnelle consistant à créer une session dans le serveur et renvoyant un cookie.
Chaque fois que l'utilisateur souhaite accéder à une route ou une ressource protégée, l'agent utilisateur doit envoyer le JWT, généralement dans l'en-tête Authorization à l'aide du schéma Bearer. Le contenu de l'en-tête doit ressembler à ce qui suit :
Authorization: Bearer <token>
Il s'agit d'un mécanisme d'authentification sans état puisque l'état de l'utilisateur n'est jamais enregistré dans la mémoire du serveur. Les routes protégées du serveur rechercheront un JWT valide dans l'en-tête Authorization, et s'il est présent, l'utilisateur sera autorisé à accéder aux ressources protégées. Comme les JWT sont autonomes, toutes les informations nécessaires sont présentes, ce qui réduit le besoin d'interroger la base de données plusieurs fois.
Cela vous permet de vous appuyer pleinement sur des API de données sans état et même d'envoyer des requêtes aux services en aval. Peu importe les domaines qui servent vos API, le partage de ressources entre origines croisées (CORS) ne sera donc pas un problème car il n'utilise pas de cookies.
Le diagramme suivant montre ce processus :
Les schémas d'authentification basés sur des jetons sont devenus extrêmement populaires ces derniers temps, car ils offrent des avantages importants par rapport aux sessions/cookies :
Certains compromis doivent être faits avec cette approche :
Le flux d'authentification JWT est très simple
Il est important de noter que les demandes d'autorisation seront incluses avec le jeton d'accès. Pourquoi est-ce important ? Eh bien, disons que les revendications d'autorisation (par exemple les privilèges utilisateur dans la base de données) sont modifiées pendant la durée de vie du jeton d'accès. Ces modifications n’entreront en vigueur qu’après l’émission d’un nouveau jeton d’accès. Dans la plupart des cas, ce n’est pas un gros problème, car les jetons d’accès sont de courte durée. Sinon, optez pour le modèle de jeton opaque.
Voyons comment implémenter l'authentification basée sur les jetons JWT à l'aide de Java et Spring, tout en essayant de réutiliser le comportement de sécurité par défaut de Spring lorsque nous le pouvons. Le framework Spring Security est livré avec des classes de plug-in qui gèrent déjà les mécanismes d'autorisation tels que : les cookies de session, HTTP Basic et HTTP Digest. Néanmoins, il lui manque un support natif pour JWT, et nous devons mettre la main à la pâte pour que cela fonctionne.
Cette démo utilise actuellement une base de données H2 appelée test_db afin que vous puissiez l'exécuter rapidement et immédiatement sans trop de configuration. Si vous souhaitez vous connecter à une autre base de données, vous devez spécifier la connexion dans le fichier application.yml
dans le répertoire des ressources. Notez que hibernate.hbm2ddl.auto=create-drop
supprimera et créera une base de données propre à chaque déploiement (vous souhaiterez peut-être la modifier si vous l'utilisez dans un projet réel). Voici l'exemple du projet, voyez avec quelle facilité vous pouvez échanger des commentaires sur les propriétés url
et dialect
pour utiliser votre propre base de données MySQL :
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
FiltreJwtToken
Le filtre JwtTokenFilter
est appliqué à chaque API ( /**
) à l'exception du point de terminaison du jeton de connexion ( /users/signin
) et du point de terminaison de connexion ( /users/signup
).
Ce filtre a les responsabilités suivantes :
JwtTokenProvider
sinon lancez une exception d'authentification Veuillez vous assurer que chain.doFilter(request, response)
est invoqué en cas d'authentification réussie. Vous souhaitez que le traitement de la demande passe au filtre suivant, car le tout dernier filtre FilterSecurityInterceptor#doFilter est chargé d'invoquer réellement la méthode dans votre contrôleur qui gère la ressource API demandée.
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
JwtTokenFilterConfigurer
Ajoute le JwtTokenFilter
au DefaultSecurityFilterChain
de la sécurité du Spring Boot.
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
Fournisseur de jetons Jwt
Le JwtTokenProvider
a les responsabilités suivantes :
Mes détails utilisateur
Implémente UserDetailsService
afin de définir notre propre fonction personnalisée loadUserbyUsername . L'interface UserDetailsService
est utilisée pour récupérer les données relatives à l'utilisateur. Il possède une méthode nommée loadUserByUsername qui recherche une entité utilisateur en fonction du nom d'utilisateur et peut être remplacée pour personnaliser le processus de recherche de l'utilisateur.
Il est utilisé par DaoAuthenticationProvider
pour charger les détails sur l'utilisateur lors de l'authentification.
WebSecurityConfig
La classe WebSecurityConfig
étend WebSecurityConfigurerAdapter
pour fournir une configuration de sécurité personnalisée.
Les beans suivants sont configurés et instanciés dans cette classe :
JwtTokenFilter
PasswordEncoder
De plus, dans la méthode WebSecurityConfig#configure(HttpSecurity http)
nous configurerons des modèles pour définir les points de terminaison d'API protégés/non protégés. Veuillez noter que nous avons désactivé la protection CSRF car nous n'utilisons pas de cookies.
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
Assurez-vous que Java 8 et Maven sont installés
Forkez ce référentiel et clonez-le
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
dans votre navigateur pour vérifier que tout fonctionne correctement. Vous pouvez modifier le port par défaut dans le fichier application.yml
server :
port : 8080
/users/me
pour vérifier que vous n'êtes pas authentifié. Vous devriez recevoir une réponse avec un 403
avec un message Access Denied
puisque vous n'avez pas encore défini votre jeton JWT valide. $ curl -X GET http://localhost:8080/users/me
/users/signin
avec l'utilisateur administrateur par défaut que nous avons créé par programme pour obtenir un jeton JWT valide $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
$ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}