web-dev-qa-db-fra.com

Comment fonctionne la chaîne de filtres de sécurité Spring

Je me rends compte que la sécurité Spring s'appuie sur une chaîne de filtres pour intercepter la requête, détecter l'authentification (en l'absence d'authentification), rediriger vers le point d'entrée de l'authentification ou transmettre la requête au service d'autorisation, puis laisser la requête toucher le servlet ou générer une exception de sécurité. (non authentifié ou non autorisé). DelegatingFitlerProxy colle ces filtres ensemble. Pour effectuer leurs tâches, ces filtres filtrent des services tels que UserDetailsService et AuthenticationManager .

Les filtres de clé dans la chaîne sont (dans l'ordre)

  • SecurityContextPersistenceFilter (restaure l'authentification à partir de JSESSIONID)
  • UsernamePasswordAuthenticationFilter (effectue l'authentification)
  • ExceptionTranslationFilter (intercepte des exceptions de sécurité à partir de FilterSecurityInterceptor)
  • FilterSecurityInterceptor (peut générer des exceptions d'authentification et d'autorisation)

Je ne comprends pas comment ces filtres sont utilisés. Est-ce que pour le formulaire-login fourni par le ressort, UsernamePasswordAuthenticationFilter n'est utilisé que pour /login , et ces derniers filtres ne sont pas? Est-ce que l'élément form-login de l'espace de noms configure automatiquement ces filtres? Est-ce que chaque demande (authentifiée ou non) atteint FilterSecurityInterceptor pour les URL sans connexion?

Et si je veux sécuriser mon REST API avec jeton JWT , qui est récupéré à partir de la connexion? = Je dois configurer la configuration de deux espaces de noms http balises, droits? Autre pour /login avec UsernamePasswordAuthenticationFilter, et un autre pour REST url, avec custom JwtAuthenticationFilter .

La configuration de deux éléments http crée-t-elle deux springSecurityFitlerChains? Est-ce que UsernamePasswordAuthenticationFilter est désactivé par défaut, jusqu'à ce que je déclare form-login? Comment remplacer SecurityContextPersistenceFilter par un qui obtiendra Authentication à partir de JWT-token existant plutôt que JSESSIONID?

98
Tuomas Toivonen

La chaîne de filtrage de sécurité Spring est un moteur très complexe et flexible.

Les filtres de clé dans la chaîne sont (dans l'ordre)

  • SecurityContextPersistenceFilter (restaure l'authentification à partir de JSESSIONID)
  • UsernamePasswordAuthenticationFilter (effectue l'authentification)
  • ExceptionTranslationFilter (intercepte des exceptions de sécurité à partir de FilterSecurityInterceptor)
  • FilterSecurityInterceptor (peut générer des exceptions d'authentification et d'autorisation)

En regardant la documentation actuelle stable de la version 4.2.1 , section 13.3 Trier les filtres , vous pouvez voir toute l'organisation du filtre de la chaîne de filtres:

13.3 Commande de filtres

L'ordre dans lequel les filtres sont définis dans la chaîne est très important. Quels que soient les filtres que vous utilisez réellement, l'ordre devrait être le suivant:

  1. ChannelProcessingFilter , car il peut être nécessaire de le rediriger vers un autre protocole

  2. SecurityContextPersistenceFilter , de sorte qu'un SecurityContext puisse être configuré dans le SecurityContextHolder au début d'une requête Web, et toute modification apportée à SecurityContext peut être copiée dans la session HttpSession à la fin de la demande Web (prête à être utilisée avec la demande Web suivante)

  3. ConcurrentSessionFilter , car il utilise la fonctionnalité SecurityContextHolder et doit mettre à jour le SessionRegistry pour refléter les demandes en cours du principal

  4. Mécanismes de traitement de l’authentification - UsernamePasswordAuthenticationFilter , CasAuthenticationFilter , BasicAuthenticationFilter etc. - afin que le SecurityContextHolder puisse être modifié pour contenir un jeton de demande d'authentification valide

  5. Le SecurityContextHolderAwareRequestFilter , si vous l'utilisez pour installer un HttpServletRequestWrapper sensible à Spring Security dans votre conteneur de servlets

  6. Le JaasApiIntegrationFilter , si un JaasAuthenticationToken est présent le SecurityContextHolder, cela traitera la FilterChain en tant que sujet dans JaasAuthenticationToken

  7. RememberMeAuthenticationFilter , de sorte que si aucun mécanisme de traitement d'authentification précédent n'a mis à jour le SecurityContextHolder, et que la demande présente un cookie qui permet aux services Remember-me d'avoir lieu, un objet d'authentification mémorisé approprié y sera placé

  8. AnonymousAuthenticationFilter , de sorte que si aucun mécanisme de traitement d'authentification antérieur n'a mis à jour le SecurityContextHolder, un objet d'authentification anonyme sera met là

  9. ExceptionTranslationFilter , pour intercepter les exceptions Spring Security afin qu'une réponse d'erreur HTTP puisse être renvoyée ou une AuthenticationEntryPoint approprié peut être lancé

  10. FilterSecurityInterceptor , pour protéger les URI Web et déclencher des exceptions lorsque l'accès est refusé

Maintenant, je vais essayer de répondre à vos questions une à une:

Je ne comprends pas comment ces filtres sont utilisés. Est-ce que pour le formulaire-login fourni par le printemps, UsernamePasswordAuthenticationFilter n'est utilisé que pour/login, et les derniers filtres ne le sont pas? L'élément d'espace de noms form-login configure-t-il automatiquement ces filtres? Est-ce que chaque demande (authentifiée ou non) atteint FilterSecurityInterceptor pour une URL sans connexion?

Une fois que vous avez configuré une section <security-http>, vous devez au moins fournir un mécanisme d'authentification pour chacune d'elles. Il doit s'agir d'un des filtres correspondant au groupe 4 de la section Filtre de la section 13.3 de la documentation de Spring Security que je viens de mentionner.

C’est la sécurité minimale valide: élément http pouvant être configuré:

<security:http authentication-manager-ref="mainAuthenticationManager" 
               entry-point-ref="serviceAccessDeniedHandler">
    <security:intercept-url pattern="/sectest/zone1/**" access="hasRole('ROLE_ADMIN')"/>
</security:http>

Pour ce faire, ces filtres sont configurés dans le proxy de la chaîne de filtres:

{
        "1": "org.springframework.security.web.context.SecurityContextPersistenceFilter",
        "2": "org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter",
        "3": "org.springframework.security.web.header.HeaderWriterFilter",
        "4": "org.springframework.security.web.csrf.CsrfFilter",
        "5": "org.springframework.security.web.savedrequest.RequestCacheAwareFilter",
        "6": "org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter",
        "7": "org.springframework.security.web.authentication.AnonymousAuthenticationFilter",
        "8": "org.springframework.security.web.session.SessionManagementFilter",
        "9": "org.springframework.security.web.access.ExceptionTranslationFilter",
        "10": "org.springframework.security.web.access.intercept.FilterSecurityInterceptor"
    }

Remarque: je les reçois en créant un RestController simple qui @Autowires FilterChainProxy et renvoie son contenu:

    @Autowired
    private FilterChainProxy filterChainProxy;

    @Override
    @RequestMapping("/filterChain")
    public @ResponseBody Map<Integer, Map<Integer, String>> getSecurityFilterChainProxy(){
        return this.getSecurityFilterChainProxy();
    }

    public Map<Integer, Map<Integer, String>> getSecurityFilterChainProxy(){
        Map<Integer, Map<Integer, String>> filterChains= new HashMap<Integer, Map<Integer, String>>();
        int i = 1;
        for(SecurityFilterChain secfc :  this.filterChainProxy.getFilterChains()){
            //filters.put(i++, secfc.getClass().getName());
            Map<Integer, String> filters = new HashMap<Integer, String>();
            int j = 1;
            for(Filter filter : secfc.getFilters()){
                filters.put(j++, filter.getClass().getName());
            }
            filterChains.put(i++, filters);
        }
        return filterChains;
    }

Ici, nous pourrions voir que simplement en déclarant l'élément <security:http> avec une configuration minimale, tous les filtres par défaut sont inclus, mais aucun d'entre eux n'est de type Authentification (4ème groupe de la section 13.3 Filtrage des filtres). Cela signifie donc que simplement en déclarant l'élément security:http, SecurityContextPersistenceFilter, ExceptionTranslationFilter et FilterSecurityInterceptor sont configurés automatiquement.

En fait, un mécanisme de traitement d'authentification devrait être configuré, et même le beans d'espaces de noms de sécurité traitant ce type de demande, générant une erreur au démarrage, mais il peut être ignoré en ajoutant un attribut entry-ref-ref dans <http:security>

Si j'ajoute un <form-login> de base à la configuration, procédez comme suit:

<security:http authentication-manager-ref="mainAuthenticationManager">
    <security:intercept-url pattern="/sectest/zone1/**" access="hasRole('ROLE_ADMIN')"/>
    <security:form-login />
</security:http>

Maintenant, le filterChain sera comme ceci:

{
        "1": "org.springframework.security.web.context.SecurityContextPersistenceFilter",
        "2": "org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter",
        "3": "org.springframework.security.web.header.HeaderWriterFilter",
        "4": "org.springframework.security.web.csrf.CsrfFilter",
        "5": "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter",
        "6": "org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter",
        "7": "org.springframework.security.web.savedrequest.RequestCacheAwareFilter",
        "8": "org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter",
        "9": "org.springframework.security.web.authentication.AnonymousAuthenticationFilter",
        "10": "org.springframework.security.web.session.SessionManagementFilter",
        "11": "org.springframework.security.web.access.ExceptionTranslationFilter",
        "12": "org.springframework.security.web.access.intercept.FilterSecurityInterceptor"
    }

Maintenant, ces deux filtres org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter et org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter sont créés et configurés dans le FilterChainProxy.

Alors, maintenant, les questions:

Est-ce que pour le formulaire-login fourni par le printemps, UsernamePasswordAuthenticationFilter n'est utilisé que pour/login, et les derniers filtres ne le sont pas?

Oui, il est utilisé pour tenter de compléter un mécanisme de traitement de connexion au cas où la demande correspond à l'URL UsernamePasswordAuthenticationFilter. Cette URL peut être configurée ou même modifiée pour correspondre à chaque requête.

Vous pouvez également avoir plusieurs mécanismes de traitement d'authentification configurés dans le même FilterchainProxy (tels que HttpBasic, CAS, etc.).

L'élément d'espace de noms form-login configure-t-il automatiquement ces filtres?

Non, l'élément form-login configure le UsernamePasswordAUthenticationFilter et, si vous ne fournissez pas d'URL de page de connexion, il configure également le fichier org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter, qui se termine par une simple connexion auto-générée. page.

Les autres filtres sont configurés automatiquement par défaut en créant simplement un élément <security:http> sans attribut security:"none".

Est-ce que chaque demande (authentifiée ou non) atteint FilterSecurityInterceptor pour une URL sans connexion?

Chaque demande doit y parvenir, car c'est l'élément qui détermine si la demande a le droit d'atteindre l'URL demandée. Mais certains des filtres déjà traités peuvent arrêter le traitement de la chaîne de filtres sans appeler FilterChain.doFilter(request, response);. Par exemple, un filtre CSRF peut arrêter le traitement de la chaîne de filtres si la demande n'a pas le paramètre csrf.

Que se passe-t-il si je veux sécuriser mon API REST avec JWT-token, qui est extraite de la connexion? Je dois configurer deux balises http de configuration d'espace de noms, des droits? Un autre pour/connectez-vous avec UsernamePasswordAuthenticationFilter, et un autre pour REST url, avec custom JwtAuthenticationFilter.

Non, vous n'êtes pas obligé de faire de cette façon. Vous pouvez déclarer à la fois UsernamePasswordAuthenticationFilter et JwtAuthenticationFilter dans le même élément http, mais cela dépend du comportement concret de chacun de ces filtres. Les deux approches sont possibles et celle à choisir dépend des préférences personnelles.

La configuration de deux éléments http crée-t-elle deux springSecurityFitlerChains?

Oui c'est vrai

Est-ce que UsernamePasswordAuthenticationFilter est désactivé par défaut, jusqu'à ce que je déclare formulaire-login?

Oui, vous pouvez le voir dans les filtres en relief dans chacun des configs que j'ai posté

Comment remplacer SecurityContextPersistenceFilter par un seul, qui obtiendra l'authentification à partir du jeton JWT existant plutôt que de JSESSIONID?

Vous pouvez éviter SecurityContextPersistenceFilter en configurant simplement stratégie de session dans <http:element>. Il suffit de configurer comme ceci:

<security:http create-session="stateless" >

Ou, dans ce cas, vous pouvez l'écraser avec un autre filtre, de cette façon dans l'élément <security:http>:

<security:http ...>  
   <security:custom-filter ref="myCustomFilter" position="SECURITY_CONTEXT_FILTER"/>    
</security:http>
<beans:bean id="myCustomFilter" class="com.xyz.myFilter" />

MODIFIER:

Une question sur "Vous pouvez également avoir plus d’un mécanisme de traitement d’authentification configuré dans le même FilterchainProxy". Ce dernier remplacera-t-il l'authentification effectuée par le premier, si plusieurs filtres d'authentification (implémentation Spring) sont déclarés? Comment cela se rapporte-t-il à plusieurs fournisseurs d'authentification?

Cela dépend finalement de l'implémentation de chaque filtre lui-même, mais il est vrai que ces derniers filtres d'authentification sont au moins capables de remplacer toute authentification antérieure éventuellement réalisée par les filtres précédents.

Mais cela n'arrivera pas nécessairement. J'ai quelques cas de production dans les services sécurisés REST où j'utilise une sorte de jeton d'autorisation qui peut être fourni à la fois en tant qu'en-tête Http ou à l'intérieur du corps de la demande. Je configure donc deux filtres qui récupèrent ce jeton, l'un à partir de l'en-tête HTTP et l'autre à partir du corps de la requête de repos. Il est vrai que si une requête http fournit ce jeton d’authentification à la fois comme en-tête HTTP et à l’intérieur du corps de la requête, les deux filtres essaieront d’exécuter le mécanisme d’authentification le déléguant au gestionnaire. déjà authentifié au début de la méthode doFilter() de chaque filtre.

Avoir plus d'un filtre d'authentification, c'est avoir plus d'un fournisseur d'authentification, mais ne le forcez pas. Dans le cas que j'ai exposé précédemment, j'ai deux filtres d'authentification, mais je n'ai qu'un seul fournisseur d'authentification, car les deux filtres créent le même type d'objet Authentification. Dans les deux cas, le gestionnaire d'authentification le délègue au même fournisseur.

Et à l'opposé, j'ai également un scénario dans lequel je ne publie qu'un seul UsernamePasswordAuthenticationFilter, mais les informations d'identification de l'utilisateur peuvent être contenues dans une base de données ou un LDAP. ensuite pour valider les identifiants.

Je pense donc qu'il est clair que ni le nombre de filtres d'authentification ni le nombre de fournisseurs d'authentification ne déterminent le nombre de filtres.

En outre, la documentation indique que SecurityContextPersistenceFilter est responsable du nettoyage de SecurityContext, qui est important en raison du regroupement de threads. Si je l'omets ou si je fournis une implémentation personnalisée, je dois implémenter le nettoyage manuellement, non? Existe-t-il d'autres erreurs similaires lors de la personnalisation de la chaîne?

Je n’avais pas examiné ce filtre avec attention auparavant, mais après votre dernière question, j’ai vérifié sa mise en œuvre et, comme d’habitude au printemps, presque tout pouvait être configuré, étendu ou écrasé.

Les SecurityContextPersistenceFilter délèguent dans une SecurityContextRepository la recherche du SecurityContext. Par défaut, un HttpSessionSecurityContextRepository est utilisé, mais cela peut être modifié à l'aide de l'un des constructeurs du filtre. Il peut donc être préférable d’écrire un SecurityContextRepository qui réponde à vos besoins et de le configurer dans SecurityContextPersistenceFilter, en faisant confiance à son comportement éprouvé plutôt que de commencer à tout créer à partir de zéro.

148
jlumietu

UsernamePasswordAuthenticationFilter n'est utilisé que pour /login, et les derniers filtres ne le sont pas?

Non, UsernamePasswordAuthenticationFilter étend AbstractAuthenticationProcessingFilter, et cela contient un RequestMatcher, ce qui signifie que vous pouvez définir votre propre URL de traitement, ce filtre ne gère que le RequestMatcher correspondant à l'URL de la demande, le L'URL de traitement par défaut est /login.

Des filtres ultérieurs peuvent toujours gérer la demande si la UsernamePasswordAuthenticationFilter exécute chain.doFilter(request, response);.

Plus de détails sur core fitler

L'élément d'espace de noms form-login configure-t-il automatiquement ces filtres?

UsernamePasswordAuthenticationFilter est créé par <form-login>, ce sont alias de filtre standard et ordre

Est-ce que chaque demande (authentifiée ou non) atteint FilterSecurityInterceptor pour une URL sans connexion?

Cela dépend si les ajusteurs précédents réussissent, mais FilterSecurityInterceptor est le dernier ajusteur normalement.

La configuration de deux éléments http crée-t-elle deux springSecurityFitlerChains?

Oui, chaque fitlerChain a un RequestMatcher, si le RequestMatcher correspond à la demande, celle-ci sera traitée par les ajusteurs de la chaîne de fitler.

La valeur par défaut RequestMatcher correspond à toutes les demandes si vous ne configurez pas le modèle ou vous pouvez configurer l'URL spécifique (<http pattern="/rest/**").

Si vous voulez en savoir plus sur les fitler, je pense que vous pouvez vérifier le code source dans Spring Security. doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)

4
chaoluo