Configuration et Analyse du Code Source de Spring Security

  1. Contexte ===========

Spring Security remplit deux fonctions essentielles : l'authentification et l'autorisation

Fonction Problème résolu Classe principale dans Spring Security
Authentification Qui êtes-vous ? AuthenticationManager
Autorisation Que pouvez-vous faire ? AuthorizationManager

Dans un article précédent, nous avons détaillé l'intégration de systèmes SSO (Single Sign-On) où le système SSO répond à la question "Qui êtes-vous ?" tandis que Spring Security gère les permissions d'accès.

Cette fois, nous nous concentrons sur les capacités d'authentification de Spring Security.

Notre scénario comprend un service A avec plusieurs endpoints répartis en trois catégories selon les utilisateurs :

  1. Utilisateurs finaux (C) : nécessitent une authentification par identifiants, avec une future intégration gateway pour la validation centralisée
  2. Utilisateurs professionnels (B) : validation via un serveur OAuth2 externe
  3. Dévelopeurs : authentification par identifiants

Version Spring Security : 5.3.x

  1. Objectif ===========

L'objectif de cet article est de présenter, à travers un cas concret, comment configurer Spring Security pour répondre à vos besoins spécifiques, en expliquant le rôle de chaque élément de configuration.

  1. Configuration Globale ========================
@Configuration
@EnableWebSecurity
public class ConfigurationSecurite {

    @Configuration
    @Order(1)
    public static class ConfigurationBSecurite extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf()
                    .disable()
                    .antMatcher("/b/**")
                    .authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .addFilterBefore(new MonFiltreJwt(), UsernamePasswordAuthenticationFilter.class);
        }

        @Override
        public void configure(WebSecurity web) {
            web.ignoring().antMatchers("/b/api/auth/access_token");
        }
    }

    @Configuration
    @Order(2)
    public static class ConfigurationCSecurite extends WebSecurityConfigurerAdapter {
        @Value("${algo-api.password}")
        private String motDePasse;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .antMatcher("/c/**")
                    .authorizeRequests()
                    .anyRequest()
                    .hasRole("C_ADMIN")
                    .and()
                    .httpBasic();
        }

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("adminC777")
                    .password(motDePasse)
                    .roles("ALGO_ADMIN");
        }
    }

    @Configuration
    @Order(3)
    public static class ConfigurationDevSecurite extends WebSecurityConfigurerAdapter {

        @Value("${swagger.password}")
        private String motDePasse;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .authorizeRequests()
                    .antMatchers("/swagger-ui/**", "/v3/api-docs/**", "/test/**")
                    .hasRole("SWAGGER_ADMIN")
                    .antMatchers("/login", "/css/**", "/js/**", "/images/**")
                    .permitAll()
                    .anyRequest()
                    .denyAll()
                    .and()
                    .formLogin();
        }

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("adminSwagger999")
                    .password(motDePasse)
                    .roles("SWAGGER_ADMIN");
        }
    }
}

3.1 Analyse de la Configuration

3.1.1 Classe ConfigurationSecurite

La classe principale ConfigurationSecurite est annotée avec @Configuration et @EnableWebSecurity. Ces annotations permettent l'intégration avec Spring Boot et l'activation des fonctionnalités Spring Security.

3.1.2 Sous-classes de WebSecurityConfigurerAdapter

Nous utilisons trois classes statiques internes : ConfigurationBSecurite, ConfigurationCSecurite et ConfigurationDevSecurite. Elles correspondent respectivement aux utilisateurs B, C et aux développeurs. Chaque classe étend WebSecurityConfigurerAdapter, ce qui permet de redéfinir les méthodes de configuration.

Selon la documentation officielle Spring Security, différents SecurityFilterChain permettent de sécuriser des préfixes d'URL différents. Ces trois classes internes représentent donc trois SecurityFilterChain distincts.

Question : Que se passe-t-il en cas de conflit d'URL ?

Lors de conflits de chemins, l'ordre de chargement des sous-classes de WebSecurityConfigurerAdapter détermine quel SecurityFilterChain sera appliqué en premier. L'annotation @Order(1), @Order(2) et @Order(3) permet de spécifier cet ordre explicitement.

Lors du débogage, nous constatons quatre SecurityFilterChain, dont un sans aucun filtre pour le chemin /b/api/auth/access_token qui a été explicitement ignoré.

Les trois méthodes principales à redéfinir dans WebSecurityConfigurerAdapter :

protected void configure(AuthenticationManagerBuilder auth)
public void configure(WebSecurity web)
protected void configure(HttpSecurity http)

La classe AuthenticationManagerBuilder configure l'authentification, que ce soit avec des identifiants en mémoire ou via une base de données JDBC.

3.1.3 Configuration HttpSecurity

C'est la classe de configuration principale avec plusieurs méthodes essentielles :

3.1.3.1 Méthodes de correspondance

Ces méthodes définissent les chemins concernés par la configuration :

  • antMatcher
  • mvcMatcher
  • regexMatcher
  • requestMatchers

requestMatchers offre une灵活ité supérieure pour la correspondance de plusieurs chemins.

antMatchers antMatcher
Classe parente AbstractRequestMatcherRegistry HttpSecurity
Paramètres Multiples wildcards URL Un seul wildcard URL
Usage Autorisation fine, permissions variibles par chemin Autorisation grossière, un SecurityFilterChain complet

3.1.3.2 Méthode authorizeRequests

Retourne un objet ExpressionUrlAuthorizationConfigurer pour activer l'autorisation fine. Deux variantes existent :

  • authorizeRequests
  • authorizeRequests(authorizeRequestsCustomizer)
Correspondance de chemins (match)
  • antMatchers : requêtes correspondant à l'expression
  • mvcMatchers
  • regexMatchers
  • anyRequest : toutes les requêtes
Droits d'accès

Après la sélection des chemins, définissez le mode d'authentification requis :

  • permitAll : accès autorisé
  • anonymous : accès anonyme
  • rememberMe : fonctionnalité "se souvenir de moi"
  • denyAll : accès refusé
  • authenticated : accès après connexion
  • hasRole : accès avec un rôle spécifique
  • hasAuthority : accès avec une autorité spécifique

3.1.3.3 Autres fonctionnalités

xxxLogin

Définit le type de connexion requis :

  • formLogin : connexion par formulaire avec pages de login personnalisées
  • oauth2Login : authentification OAuth 2.0 (par exemple via支付宝)
sessionManagement

Gère les sessions utilisateur

addFilterxxx

Ajoute un filtre personnalisé à la chaîne de filtres Spring Security. En réalité, toutes les fonctionnalités de Spring Security reposent sur des filtres. Lorsque les filtres existants ne répondent pas à vos besoins, vous pouvez implémenter vos propres filtres.

3.1.4 Configuration WebSecurity

WebSecurity est une configuration globale avec des méthodes courantes :

  • ignoring : exclut des chemins de la sécurité
  • httpFirewall : configure un pare-feu pour bloquer les requêtes invalides
  • debug : active les logs de débogage

Dans notre exemple, nous ignorons le chemin /b/api/auth/access_token.

  1. Analyse du Code Source =========================

L'architecture globale comprend :

  • SecurityBuilder et SecurityConfigurer : interfaces centrales, le premier implémente le pattern Builder, le second personnalise la configuration
  • HttpSecurity et WebSecurity : deux implémentations principales de SecurityBuilder
  • XXXXSecurityConfigurer : série de classes de configuration pour CSRF, CORS, connexion, session
  • WebSecurityConfigurerAdapter : adaptateur permettant de personnaliser HttpSecurity et WebSecurity
  • WebSecurityConfiguration : classe d'auto-configuration, point de départ de Spring Security

4.1 EnableWebSecurity

L'annotation @EnableWebSecurity est indispensible. Elle utilise @Import pour charger la classe de configuration WebSecurityConfiguration :

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
        SpringWebMvcImportSelector.class,
        OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
    boolean debug() default false;
}

4.2 WebSecurityConfiguration

Cette classe constitue un point clé. Via l'annotation @Value, elle injecte une List<SecurityConfigurer<Filter, WebSecurity>> qui correspond à nos classes étendues de WebSecurityConfigurerAdapter. Ces classes sont collectées, triées et intégrées à l'objet webSecurity.

public void setFilterChainProxySecurityConfigurer(
        ObjectPostProcessor<Object> objectPostProcessor,
        @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}")
        List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
        throws Exception {
    webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));
    if (debugEnabled != null) {
        webSecurity.debug(debugEnabled);
    }
    webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);

    Integer previousOrder = null;
    Object previousConfig = null;
    for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
        Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
        if (previousOrder != null && previousOrder.equals(order)) {
            throw new IllegalStateException(
                "@Order on WebSecurityConfigurers must be unique.");
        }
        previousOrder = order;
        previousConfig = config;
    }
    for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
        webSecurity.apply(webSecurityConfigurer);
    }
    this.webSecurityConfigurers = webSecurityConfigurers;
}

Un Servlet Filter est ensuite enregistré pour implémenter toutes les fonctionnalités Spring Security :

@Bean(name = {"springSecurityFilterChain"})
public Filter springSecurityFilterChain() throws Exception {
    boolean hasConfigurers = this.webSecurityConfigurers != null
        && !this.webSecurityConfigurers.isEmpty();
    if (!hasConfigurers) {
        WebSecurityConfigurerAdapter adapter =
            (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor
                .postProcess(new WebSecurityConfigurerAdapter() {});
        this.webSecurity.apply(adapter);
    }
    return (Filter)this.webSecurity.build();
}

4.3 WebSecurity

Cette classe applique principalement le pattern Builder, en填充 divers attributs et en appelant HttpSecurity pour construire le SecurityFilterChain.

protected Filter performBuild() throws Exception {
    Assert.state(!securityFilterChainBuilders.isEmpty(),
        () -> "At least one SecurityBuilder needs to be specified.");
    int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
    List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
    for (RequestMatcher ignoredRequest : ignoredRequests) {
        securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
    }
    for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder
            : securityFilterChainBuilders) {
        securityFilterChains.add(securityFilterChainBuilder.build());
    }
    FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
    if (httpFirewall != null) {
        filterChainProxy.setFirewall(httpFirewall);
    }
    filterChainProxy.afterPropertiesSet();
    Filter result = filterChainProxy;
    if (debugEnabled) {
        logger.warn("\n\n****************************************************************\n"
            + "********** Security debugging is enabled. *************\n"
            + "********** This may include sensitive information. *************\n"
            + "********** Do not use in a production system! *************\n"
            + "****************************************************************\n\n");
        result = new DebugFilter(filterChainProxy);
    }
    postBuildAction.run();
    return result;
}

4.4 HttpSecurity

Cette classe est également simple dans sa structure :

protected DefaultSecurityFilterChain performBuild() {
    this.filters.sort(this.comparator);
    return new DefaultSecurityFilterChain(this.requestMatcher, this.filters);
}

La logique principale se trouve dans la classe parente AbstractConfiguredSecurityBuilder. Les méthodes init() et configure() activent réellement les configurations personnalisées :

protected final O doBuild() throws Exception {
    synchronized(this.configurers) {
        this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
        this.beforeInit();
        this.init();
        this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
        this.beforeConfigure();
        this.configure();
        this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
        O result = (O)this.performBuild();
        this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
        return result;
    }
}

private void init() throws Exception {
    for(SecurityConfigurer<O, B> configurer : this.getConfigurers()) {
        configurer.init(this);
    }
    for(SecurityConfigurer<O, B> configurer : this.configurersAddedInInitializing) {
        configurer.init(this);
    }
}

private void configure() throws Exception {
    for(SecurityConfigurer<O, B> configurer : this.getConfigurers()) {
        configurer.configure(this);
    }
}

4.5 XXXXSecurityConfigurer

Lors de l'appel de la méthode cors() sur HttpSecurity, le CorsConfigurer correspondant est enregistré. Ce configurateur sera ensuite initialisé et configuré lors des phases init() et configure().

public CorsConfigurer<HttpSecurity> cors() throws Exception {
    return (CorsConfigurer)this.getOrApply(new CorsConfigurer());
}

Voici l'implémentation du configure dans CorsConfigurer : un filtre est créé puis ajouté à HttpSecurity. Ce filtre résout les problèmes de cross-origin (CORS).

public void configure(H http) {
    ApplicationContext context = (ApplicationContext)http.getSharedObject(
        ApplicationContext.class);
    CorsFilter corsFilter = this.getCorsFilter(context);
    if (corsFilter == null) {
        throw new IllegalStateException(
            "Please configure either a corsFilter bean or a corsConfigurationSource bean.");
    } else {
        http.addFilter(corsFilter);
    }
}

Bien que certains SecurityConfigurer puissent avoir une logique complexe, le principe fondamental reste le même : ajouter des filtres à la chaîne pour gérer les requêtes entrantes.

Annexe

Comment récupérer et compiler le code source de Spring Security

Pour cloner le dépôt sans l'historique (plus rapide et moins lourd), spécifiez la branche :

git clone --depth 1 https://github.com/spring-projects/spring-security.git -b 5.3.x

Lors du téléchargement des dépendances, certains plugins Spring peuvent échouer car le repository Spring a restreint l'accès public fin 2021.

Références :

Bien que les miroirs soient indiqués comme disponibles, des problèmes de synchronisation persistent. Il est nécessaire de modifier le fichier build.gradle :

buildscript {
    configurations.all {
        resolutionStrategy.dependencySubstitution {
            substitute module('org.apache.xerces:xercesImpl') with module('xerces:xercesImpl:2.9.1')
            substitute module('org.apache.xerces:resolver') with module('xerces:resolver:2.9.1')
        }
    }
    dependencies {
        classpath 'io.spring.gradle:spring-build-conventions:0.0.38-SNAPSHOT'
        classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
        classpath 'io.spring.nohttp:nohttp-gradle:0.0.10'
        classpath "io.freefair.gradle:aspectj-plugin:4.1.6"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
    }
    repositories {
        maven { url 'https://maven.aliyun.com/repository/spring-plugin' }
        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
        maven { url = 'https://repo.spring.io/plugins-snapshot' }
        maven { url = 'https://repo.spring.io/plugins-milestone' }
        maven { url = 'https://repo.spring.io/plugins-release' }
        maven { url = 'https://plugins.gradle.org/m2/' }
    }
}

apply plugin: 'io.spring.nohttp'
apply plugin: 'locks'
apply plugin: 'io.spring.convention.root'
apply plugin: 'org.jetbrains.kotlin.jvm'

group = 'org.springframework.security'
description = 'Spring Security'

ext.snapshotBuild = version.contains("SNAPSHOT")
ext.releaseBuild = version.contains("SNAPSHOT")
ext.milestoneBuild = !(snapshotBuild || releaseBuild)

dependencyManagementExport.projects = subprojects.findAll { !it.name.contains('-boot') }

repositories {
    mavenCentral()
}

subprojects {
    plugins.withType(JavaPlugin) {
        project.sourceCompatibility='1.8'
    }
    tasks.withType(JavaCompile) {
        options.encoding = "UTF-8"
    }
}

Les modifications principales concernent :

  1. La mise à jour de la version du plugin spring-build-conventions
  2. L'ajout des dépôts de plugins Alibaba Cloud

Note : La synchronisation complète des dépendances peut échouer en raison de l'inaccessibilité de certains dépôts Spring. Cependant, la navigation vers certaines classes reste possible.

Étiquettes: spring-security Java authentication authorization spring-boot

Publié le 31 juillet à 07h40