Dans cet article, nous explorerons la sécurité Spring et créerons un système d'authentification avec OAuth 2.0.
Avant de plonger dans le fonctionnement de Spring Security, il est crucial de comprendre le cycle de vie de gestion des requêtes dans un serveur Web basé sur Java. Spring Security s'intègre de manière transparente dans ce cycle de vie pour sécuriser les demandes entrantes.
Le cycle de vie de la gestion d'une requête HTTP dans une application basée sur Spring avec Spring Security implique plusieurs étapes, chacune jouant un rôle critique dans le traitement, la validation et la sécurisation de la requête.
Le cycle de vie commence lorsqu'un client (par exemple, un navigateur, une application mobile ou un outil API comme Postman) envoie une requête HTTP au serveur.
Exemple :
OBTENIR /api/admin/dashboard HTTP/1.1
Le conteneur de servlet (par exemple, Tomcat) reçoit la requête et la délègue au DispatcherServlet, le contrôleur frontal d'une application Spring. C’est ici que commence le pipeline de traitement de l’application.
Avant que DispatcherServlet ne traite la demande, La chaîne de filtrage de Spring Security l'intercepte. La chaîne de filtres est une séquence de filtres, chacun étant chargé de gérer des tâches de sécurité spécifiques. Ces filtres garantissent que la demande répond aux exigences d'authentification et d'autorisation avant d'atteindre la logique de l'application.
Filtres d'authentification :
Ces filtres vérifient si la demande contient des informations d'identification valides, telles qu'un nom d'utilisateur/mot de passe, un JWT ou des cookies de session.
Filtres d'autorisation :
Après l'authentification, ces filtres garantissent que l'utilisateur authentifié dispose des rôles ou des autorisations nécessaires pour accéder à la ressource demandée.
Autres filtres :
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
Si l'authentification réussit, Spring Security crée un objet d'authentification et le stocke dans le SecurityContext. Cet objet, souvent stocké dans un stockage local de thread, est accessible tout au long du cycle de vie de la requête.
Principal : représente l'utilisateur authentifié (par exemple, le nom d'utilisateur).
Identifiants : inclut les détails d'authentification tels que les jetons JWT ou les mots de passe.
Autorités : contient les rôles et les autorisations attribués à l'utilisateur.
Une requête passe par les filtres d'authentification.
Si les informations d'identification sont valides, l'objet Authentification est créé et ajouté au SecurityContext.
Si les informations d'identification ne sont pas valides, ExceptionTranslationFilter envoie une réponse 401 non autorisée au client.
Une fois que la requête passe avec succès via la chaîne de filtrage de sécurité Spring, le DispatcherServlet prend le relais :
Mappage des gestionnaires :
Il mappe la requête entrante à la méthode de contrôleur appropriée en fonction de l'URL et de la méthode HTTP.
Invocation du contrôleur :
Le contrôleur mappé traite la demande et renvoie la réponse appropriée, souvent avec l'aide d'autres composants Spring tels que les services et les référentiels.
Spring Security s'intègre dans ce cycle de vie grâce à ses filtres, interceptant les requêtes au plus tôt. Au moment où une requête atteint la logique de l'application, elle a déjà été authentifiée et autorisée, garantissant que seul le trafic légitime est traité par l'application principale.
La conception de Spring Security garantit que l'authentification, l'autorisation et d'autres mesures de sécurité sont gérées de manière déclarative, donnant aux développeurs la flexibilité de personnaliser ou d'étendre son comportement selon les besoins. Il applique non seulement les meilleures pratiques, mais simplifie également la mise en œuvre d'exigences de sécurité complexes dans les applications modernes.
Après avoir exploré la Chaîne de filtres dans Spring Security, examinons d'autres composants clés qui jouent un rôle central dans le processus d'authentification et d'autorisation.
AuthenticationManager est une interface qui définit une méthode unique, Authenticate(Authentication Authentication) , qui est utilisée pour vérifier les informations d'identification d'un utilisateur et déterminer si elles sont valides. Vous pouvez considérer AuthenticationManager comme un coordinateur dans lequel vous pouvez enregistrer plusieurs fournisseurs et, en fonction du type de demande, il enverra une demande d'authentification au bon fournisseur.
Un AuthenticationProvider est une interface qui définit un contrat pour authentifier les utilisateurs en fonction de leurs informations d'identification. Il représente un mécanisme d'authentification spécifique, tel que nom d'utilisateur/mot de passe, OAuth ou LDAP. Plusieurs implémentations d'AuthenticationProvider peuvent coexister, permettant à l'application de prendre en charge diverses stratégies d'authentification.
Objet d'authentification :
Le AuthenticationProvider traite un objet d'authentification, qui encapsule les informations d'identification de l'utilisateur (par exemple, le nom d'utilisateur et le mot de passe).
Méthode d'authentification :
Chaque AuthenticationProvider implémente la méthode authentifier (authentification d'authentification), où réside la logique d'authentification réelle. Cette méthode :
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
Un AuthenticationProvider basé sur une base de données valide les noms d'utilisateur et les mots de passe.
Un AuthenticationProvider basé sur OAuth valide les jetons émis par un fournisseur d'identité externe.
UserDetailsService est décrit comme une interface principale qui charge des données spécifiques à l'utilisateur dans la documentation Spring. Il contient une seule méthode loadUserByUsername qui accepte le nom d'utilisateur comme paramètre et renvoie l'objet d'identité ==User==. Fondamentalement, nous créons une classe d'implémentation de UserDetailsService dans laquelle nous remplaçons la méthode loadUserByUsername.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
Maintenant, comment ces trois fonctionnent ensemble, AuthenticationManager demandera à AuthenticationProvider de procéder à l'authentification en fonction du type de fournisseur spécifié et l'implémentation de UserDetailsService aidera AuthenticationProvider à prouver les détails de l'utilisateur.
L'utilisateur envoie une demande au point de terminaison authentifié avec ses informations d'identification (nom d'utilisateur et mot de passe) ou un jeton JWT (dans l'en-tête) et la demande est transmise au filtre d'authentification
AuthenticationFilter (par exemple, UsernamePasswordAuthenticationFilter) :
Ce filtre personnalisé étend OncePerRequestFilter et est placé avant le UsernamePasswordAuthenticationFilter , et il extrait le jeton de la demande et le valide.
Si le jeton est valide, il crée un UsernamePasswordAuthenticationToken et définit ce jeton dans le contexte de sécurité qui indique à la sécurité Spring que la demande est authentifiée et lorsque cette demande passe au UsernamePasswordAuthenticationFilter, elle passe simplement car elle a le UsernamePasswordAuthenticationToken
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
ce UsernamePasswordAuthenticationToken est généré à l'aide de AuthenticationManager et AuthenticationProvider si nous avons transmis le nom d'utilisateur et le mot de passe au lieu du jeton après avoir authentifié le nom d'utilisateur et le mot de passe à l'aide de notre classe UserDetails.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
UserDetailsService : AuthentificationProvider utilise UserDetailsService pour charger les détails de l'utilisateur en fonction du nom d'utilisateur. Et nous fournissons cela avec une implémentation de UserDetailsService
Validation des informations d'identification : elle compare le mot de passe fourni avec celui stocké dans les détails de l'utilisateur (généralement à l'aide d'un PasswordEncoder).
package com.oauth.backend.services; import com.oauth.backend.entities.User; import com.oauth.backend.repositories.UserRepository; import lombok.RequiredArgsConstructor; 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.Component; @Component public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ throw new UsernameNotFoundException(username); } return new UserDetailsImpl(user); } public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if(user==null){ throw new UsernameNotFoundException(email); } return new UserDetailsImpl(user); } }
Maintenant, tous ces différents filtres et beans doivent être configurés pour que la sécurité Spring sache quoi faire, c'est pourquoi nous créons une classe de configuration dans laquelle nous spécifions toute la configuration.
@Component public class JWTFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserDetailsService userDetailsService; public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if(authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } final String jwt = authHeader.substring(7); final String userName = jwtService.extractUserName(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(userName !=null && authentication == null) { //Authenticate UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(jwtService.isTokenValid(jwt,userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } }
Jusqu'à présent, nous avons compris et configuré notre authentification avec l'aide de Spring Security, il est maintenant temps de la tester.
Nous allons créer une application simple avec deux contrôleurs AuthController (gère la connexion et l'enregistrement) et ProductController (contrôleur factice protégé)
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
package com.oauth.backend.services; import com.oauth.backend.entities.User; import com.oauth.backend.repositories.UserRepository; import lombok.RequiredArgsConstructor; 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.Component; @Component public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ throw new UsernameNotFoundException(username); } return new UserDetailsImpl(user); } public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if(user==null){ throw new UsernameNotFoundException(email); } return new UserDetailsImpl(user); } }
@Component public class JWTFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserDetailsService userDetailsService; public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if(authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } final String jwt = authHeader.substring(7); final String userName = jwtService.extractUserName(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(userName !=null && authentication == null) { //Authenticate UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(jwtService.isTokenValid(jwt,userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{ return config.getAuthenticationManager(); }
@Bean public AuthenticationProvider authenticationProvider(){ DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsServiceImpl); authenticationProvider.setPasswordEncoder(passwordEncoder); return authenticationProvider; }
Jusqu'à présent, nous avons mis en place une inscription, une connexion et une vérification, mais que se passe-t-il si je souhaite également ajouter la fonctionnalité de connexion avec Google/Github, nous pouvons le faire avec l'aide d'OAuth2.0
OAuth 2.0 est un protocole d'autorisation permettant aux utilisateurs d'accorder à des applications tierces l'accès aux ressources stockées sur d'autres plateformes (par exemple Google Drive, Github) sans partager les informations d'identification de ces plateformes.
Il est principalement utilisé pour activer les connexions sociales telles que « Connexion avec Google », « Connexion avec github ».
Des plateformes comme Google, Facebook, Github fournissent un serveur d'autorisation qui implémente le protocole OAuth 2.0 pour cette connexion sociale ou l'autorisation d'accès.
Nous allons maintenant examiner chaque concept un par un
Le propriétaire de la ressource est l'utilisateur qui souhaite autoriser l'application tierce (votre application).
C'est votre application (tierce) qui souhaite accéder aux données ou ressources du serveur de ressources.
Il s'agit du serveur sur lequel les données de l'utilisateur sont stockées et auxquelles l'application tierce doit accéder.
Le serveur qui authentifie le propriétaire de la ressource et émet des jetons d'accès au client (par exemple, les comptes Google).
Un identifiant délivré par le serveur d'autorisation au client, lui permettant d'accéder au serveur de ressources au nom de l'utilisateur. Il est généralement de courte durée et expire très bientôt, donc un jeton d'actualisation est également fourni afin d'actualiser ce jeton d'accès afin que l'utilisateur n'ait pas besoin d'autoriser à nouveau.
Autorisations spécifiques accordées par l'utilisateur, définissant ce que le client peut et ne peut pas faire avec les données de l'utilisateur. Par exemple, pour l'autorisation, nous n'avons besoin que d'informations utilisateur telles que le profil, le nom, etc., mais pour l'accès aux fichiers, une portée différente est requise.
Il fait référence aux méthodes par lesquelles l'application client peut obtenir le jeton d'accès du serveur d'autorisation. Une subvention définit le processus et les conditions dans lesquels l'application client est autorisée à accéder aux données protégées d'un propriétaire de ressource.
Il est sécurisé car nous n'avons pas besoin d'exposer notre secret client et d'autres informations d'identification au navigateur
Il existe deux types de subventions principalement utilisés fournis par OAuth 2.0
Il s'agit du type de subvention/méthode le plus utilisé, le plus sécurisé et destiné aux applications côté serveur
En cela, un code d'autorisation est donné par le client au backend et le backend donne le jeton d'accès au client.
Processus :
Utilisé par les applications à page unique (SPA) ou les applications sans backend. En cela, le jeton d'accès est directement généré et émis dans le navigateur lui-même.
Processus :
Nous mettrons en œuvre les deux séparément pour une compréhension complète, mais nous mettrons d'abord en œuvre l'octroi de code d'autorisation pour cela dont nous aurons besoin
Serveur d'autorisation
Il peut s'agir d'une plate-forme (comme Google, github), ou vous pouvez également créer la vôtre en utilisant KeyCloak, ou vous pouvez également créer la vôtre en respectant les normes OAuth 2.0 (nous pourrions le faire dans le prochain blog ?)
Application Spring Boot
Ce sera notre application/service principal backend qui gérera toutes les opérations telles que l'échange de code, la vérification, l'enregistrement des détails de l'utilisateur et l'attribution de jetons JWT
Application React (Frontend)
Ce sera notre client qui redirigera l'utilisateur vers le serveur d'autorisation pour autorisation.
Donc, dans notre implémentation, ce que nous allons faire, c'est que le frontend (web/app) redirigera notre utilisateur vers la connexion Google avec l'uri de redirection vers notre point de terminaison backend qui prendra le contrôle plus loin, nous en reparlerons plus tard et avec le redirect_url nous transmettons également l'identifiant client de notre application, tout cela sera envoyé dans les paramètres de requête.
Non, lorsque l'utilisateur se connectera avec succès à Google, le serveur d'autorisation (celui de Google) redirigera notre demande vers le point de terminaison backend et là, ce que nous ferons, c'est échanger le code d'autorisation avec le serveur d'autorisation pour obtenir le jeton d'accès et actualiser le jeton, puis nous pouvons gérer l'authentification comme nous le souhaitons et enfin nous enverrons une réponse à notre frontend qui aura un cookie et redirigera vers notre tableau de bord ou peut être une page protégée.
Nous allons maintenant examiner le code, mais assurez-vous d'ajouter l'URL de votre point de terminaison backend dans les URL de redirection autorisées dans le tableau de bord de la console Google pour le client OAuth.
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
et voilà, cela fonctionnera bien et pour tester, vous pouvez créer une simple application frontale qui n'aura rien d'autre qu'un contexte et vous connaissez les fonctions de connexion et d'enregistrement.
Merci d'avoir lu jusqu'ici, et si vous avez des suggestions, n'hésitez pas à les déposer dans les commentaires
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!