Comment puis-je utiliser Spring Security sans sessions?

99

Je crée une application Web avec Spring Security qui fonctionnera sur Amazon EC2 et utilisera les équilibreurs de charge élastiques d'Amazon. Malheureusement, ELB ne prend pas en charge les sessions persistantes, je dois donc m'assurer que mon application fonctionne correctement sans sessions.

Jusqu'à présent, j'ai configuré RememberMeServices pour attribuer un jeton via un cookie, et cela fonctionne bien, mais je veux que le cookie expire avec la session du navigateur (par exemple, lorsque le navigateur se ferme).

Je dois imaginer que je ne suis pas le premier à vouloir utiliser Spring Security sans sessions ... des suggestions?

Jarrod Carlson
la source

Réponses:

124

Dans Spring Security 3 avec Java Config , vous pouvez utiliser HttpSecurity.sessionManagement () :

@Override
protected void configure(final HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Ben Hutchison
la source
2
C'est la bonne réponse pour la configuration Java, reflétant ce que @sappenin a correctement déclaré pour la configuration xml dans un commentaire sur la réponse acceptée. Nous utilisons cette méthode et notre application est en effet sans session.
Paul
Cela a un effet secondaire. Le conteneur Tomcat ajoutera "; jsessionid = ..." aux demandes d'images, de feuilles de style, etc., car Tomcat n'aime pas être sans état, et Spring Security bloquera ensuite ces actifs lors du premier chargement car "l'URL contenait un String potentiellement malveillant ';' ".
workerjoe
@workerjoe Alors, qu'est-ce que vous essayez de dire par cette configuration java, les sessions ne sont pas créées par spring security plutôt par tomcat?
Vishwas Atrey
@VishwasAtrey Dans ma compréhension (ce qui peut être faux), Tomcat crée et maintient les sessions. Spring en profite, ajoutant ses propres données. J'ai essayé de créer une application Web sans état et cela n'a pas fonctionné, comme je l'ai mentionné ci-dessus. Voir cette réponse à ma propre question pour en savoir plus.
workerjoe
28

Cela semble être encore plus facile dans Spring Securitiy 3.0. Si vous utilisez la configuration de l'espace de noms, vous pouvez simplement procéder comme suit:

<http create-session="never">
  <!-- config -->
</http>

Ou vous pouvez configurer SecurityContextRepository comme null, et rien ne serait jamais enregistré de cette façon également .

Jarrod Carlson
la source
5
Cela n'a pas fonctionné comme je le pensais. Au lieu de cela, il y a un commentaire ci-dessous qui fait la distinction entre «jamais» et «apatride». En utilisant "jamais", mon application créait toujours des sessions. En utilisant "sans état", mon application est devenue sans état et je n'ai pas eu besoin de mettre en œuvre les remplacements mentionnés dans d'autres réponses. Voir le numéro JIRA ici: jira.springsource.org/browse/SEC-1424
sappenin le
27

Nous avons travaillé sur le même problème (injecter un SecurityContextRepository personnalisé à SecurityContextPersistenceFilter) pendant 4 à 5 heures aujourd'hui. Finalement, nous l'avons compris. Tout d'abord, dans la section 8.3 de Spring Security réf. doc, il existe une définition de bean SecurityContextPersistenceFilter

<bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
    <property name='securityContextRepository'>
        <bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
            <property name='allowSessionCreation' value='false' />
        </bean>
    </property>
</bean>

Et après cette définition, il y a cette explication: "Sinon, vous pouvez fournir une implémentation nulle de l'interface SecurityContextRepository, qui empêchera le contexte de sécurité d'être stocké, même si une session a déjà été créée pendant la requête."

Nous devions injecter notre SecurityContextRepository personnalisé dans SecurityContextPersistenceFilter. Nous avons donc simplement changé la définition du bean ci-dessus avec notre impl personnalisé et l'avons placée dans le contexte de sécurité.

Lorsque nous exécutons l'application, nous avons tracé les journaux et constaté que SecurityContextPersistenceFilter n'utilisait pas notre impl personnalisé, il utilisait le HttpSessionSecurityContextRepository.

Après quelques autres choses que nous avons essayées, nous avons compris que nous devions donner notre implément SecurityContextRepository personnalisé avec l'attribut "security-context-repository-ref" de l'espace de noms "http". Si vous utilisez l'espace de noms "http" et que vous souhaitez injecter votre propre implément SecurityContextRepository, essayez l'attribut "security-context-repository-ref".

Lorsque l'espace de noms "http" est utilisé, une définition SecurityContextPersistenceFilter distincte est ignorée. Comme je l'ai copié ci-dessus, le doc de référence. ne dit pas cela.

Veuillez me corriger si j'ai mal compris les choses.

Basri Kahveci
la source
Merci, ce sont des informations précieuses. Je vais l'essayer dans mon application.
Jeff Evans
Merci, c'est ce dont j'avais besoin avec le printemps 3.0
Justin Ludwig
1
Vous êtes assez précis quand vous dites que l'espace de noms http ne permet pas un SecurityContextPersistenceFilter personnalisé, il m'a fallu quelques heures de débogage pour le comprendre
Jaime Hablutzel
Merci beaucoup pour cette contribution! J'étais sur le point d'arracher mes petits cheveux. Je me demandais pourquoi la méthode setSecurityContextRepository de SecurityContextPersistenceFilter était obsolète (la documentation disant d'utiliser l'injection de constructeur, ce qui n'est pas correct non plus).
fool4jesus
10

Jetez un œil à la SecurityContextPersistenceFilterclasse. Il définit comment le SecurityContextHolderest peuplé. Par défaut, il utilise HttpSessionSecurityContextRepositorypour stocker le contexte de sécurité dans la session http.

J'ai implémenté ce mécanisme assez facilement, avec custom SecurityContextRepository.

Voir securityContext.xmlci - dessous:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
       http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">

    <context:annotation-config/>

    <sec:global-method-security secured-annotations="enabled" pre-post-annotations="enabled"/>

    <bean id="securityContextRepository" class="com.project.server.security.TokenSecurityContextRepository"/>

    <bean id="securityContextFilter" class="com.project.server.security.TokenSecurityContextPersistenceFilter">
        <property name="repository" ref="securityContextRepository"/>
    </bean>

    <bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
        <constructor-arg value="/login.jsp"/>
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
            </list>
        </constructor-arg>
    </bean>

    <bean id="formLoginFilter"
          class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="authenticationSuccessHandler">
            <bean class="com.project.server.security.TokenAuthenticationSuccessHandler">
                <property name="defaultTargetUrl" value="/index.html"/>
                <property name="passwordExpiredUrl" value="/changePassword.jsp"/>
                <property name="alwaysUseDefaultTargetUrl" value="true"/>
            </bean>
        </property>
        <property name="authenticationFailureHandler">
            <bean class="com.project.server.modules.security.CustomUrlAuthenticationFailureHandler">
                <property name="defaultFailureUrl" value="/login.jsp?failure=1"/>
            </bean>
        </property>
        <property name="filterProcessesUrl" value="/j_spring_security_check"/>
        <property name="allowSessionCreation" value="false"/>
    </bean>

    <bean id="servletApiFilter"
          class="org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter"/>

    <bean id="anonFilter" class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
        <property name="key" value="ClientApplication"/>
        <property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
    </bean>


    <bean id="exceptionTranslator" class="org.springframework.security.web.access.ExceptionTranslationFilter">
        <property name="authenticationEntryPoint">
            <bean class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
                <property name="loginFormUrl" value="/login.jsp"/>
            </bean>
        </property>
        <property name="accessDeniedHandler">
            <bean class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                <property name="errorPage" value="/login.jsp?failure=2"/>
            </bean>
        </property>
        <property name="requestCache">
            <bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
        </property>
    </bean>

    <alias name="filterChainProxy" alias="springSecurityFilterChain"/>

    <bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
        <sec:filter-chain-map path-type="ant">
            <sec:filter-chain pattern="/**"
                              filters="securityContextFilter, logoutFilter, formLoginFilter,
                                        servletApiFilter, anonFilter, exceptionTranslator, filterSecurityInterceptor"/>
        </sec:filter-chain-map>
    </bean>

    <bean id="filterSecurityInterceptor"
          class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
        <property name="securityMetadataSource">
            <sec:filter-security-metadata-source use-expressions="true">
                <sec:intercept-url pattern="/staticresources/**" access="permitAll"/>
                <sec:intercept-url pattern="/index.html*" access="hasRole('USER_ROLE')"/>
                <sec:intercept-url pattern="/rpc/*" access="hasRole('USER_ROLE')"/>
                <sec:intercept-url pattern="/**" access="permitAll"/>
            </sec:filter-security-metadata-source>
        </property>
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="accessDecisionManager" ref="accessDecisionManager"/>
    </bean>

    <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
        <property name="decisionVoters">
            <list>
                <bean class="org.springframework.security.access.vote.RoleVoter"/>
                <bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
            </list>
        </property>
    </bean>

    <bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
        <property name="providers">
            <list>
                <bean name="authenticationProvider"
                      class="com.project.server.modules.security.oracle.StoredProcedureBasedAuthenticationProviderImpl">
                    <property name="dataSource" ref="serverDataSource"/>
                    <property name="userDetailsService" ref="userDetailsService"/>
                    <property name="auditLogin" value="true"/>
                    <property name="postAuthenticationChecks" ref="customPostAuthenticationChecks"/>
                </bean>
            </list>
        </property>
    </bean>

    <bean id="customPostAuthenticationChecks" class="com.project.server.modules.security.CustomPostAuthenticationChecks"/>

    <bean name="userDetailsService" class="com.project.server.modules.security.oracle.UserDetailsServiceImpl">
        <property name="dataSource" ref="serverDataSource"/>
    </bean>

</beans>
Lukas Herman
la source
1
Salut Lukas, pouvez-vous donner plus de détails sur l'implémentation de votre référentiel de contexte de sécurité?
Jim Downing
1
La classe TokenSecurityContextRepository contient HashMap <String, SecurityContext> contextMap. Dans loadContext (), la méthode vérifie si SecurityContext existe pour le code de hachage de session passé par requestParameter sid, ou cookie, ou requestHeader personnalisé ou une combinaison de ceux ci-dessus. Renvoie SecurityContextHolder.createEmptyContext () si le contexte n'a pas pu être résolu. La méthode saveContext place le contexte résolu dans contextMap.
Lukas Herman
8

En fait, create-session="never"cela ne signifie pas être complètement apatride. Il y a un problème pour cela dans la gestion des problèmes de Spring Security.

hleinone
la source
3

Après avoir lutté avec les nombreuses solutions publiées dans cette réponse, pour essayer de faire fonctionner quelque chose lors de l'utilisation de la <http>configuration de l' espace de noms, j'ai finalement trouvé une approche qui fonctionne réellement pour mon cas d'utilisation. Je n'ai pas vraiment besoin que Spring Security ne démarre pas une session (parce que j'utilise session dans d'autres parties de l'application), mais simplement qu'il ne se "souvienne" pas du tout de l'authentification dans la session (il devrait être revérifié chaque demande).

Pour commencer, je n'ai pas pu comprendre comment faire la technique "d'implémentation nulle" décrite ci-dessus. Il n'était pas clair si vous êtes censé définir le securityContextRepository sur nullou sur une implémentation sans opération. Le premier ne fonctionne pas car un NullPointerExceptionest jeté à l'intérieur SecurityContextPersistenceFilter.doFilter(). En ce qui concerne l'implémentation no-op, j'ai essayé de l'implémenter de la manière la plus simple que je puisse imaginer:

public class NullSpringSecurityContextRepository implements SecurityContextRepository {

    @Override
    public SecurityContext loadContext(final HttpRequestResponseHolder requestResponseHolder_) {
        return SecurityContextHolder.createEmptyContext();
    }

    @Override
    public void saveContext(final SecurityContext context_, final HttpServletRequest request_,
            final HttpServletResponse response_) {
    }

    @Override
    public boolean containsContext(final HttpServletRequest request_) {
        return false;
    }

}

Cela ne fonctionne pas dans mon application, à cause d'un étrange ClassCastExceptionrapport avec le response_type.

Même en supposant que j'ai réussi à trouver une implémentation qui fonctionne (en ne stockant simplement pas le contexte en session), il reste le problème de savoir comment l'injecter dans les filtres construits par la <http>configuration. Vous ne pouvez pas simplement remplacer le filtre à la SECURITY_CONTEXT_FILTERposition, comme indiqué dans la documentation . Le seul moyen que j'ai trouvé pour accrocher dans le SecurityContextPersistenceFilterqui est créé sous les couvertures était d'écrire un ApplicationContextAwareharicot laid :

public class SpringSecuritySessionDisabler implements ApplicationContextAware {

    private final Logger logger = LoggerFactory.getLogger(SpringSecuritySessionDisabler.class);

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext_) throws BeansException {
        applicationContext = applicationContext_;
    }

    public void disableSpringSecuritySessions() {
        final Map<String, FilterChainProxy> filterChainProxies = applicationContext
                .getBeansOfType(FilterChainProxy.class);
        for (final Entry<String, FilterChainProxy> filterChainProxyBeanEntry : filterChainProxies.entrySet()) {
            for (final Entry<String, List<Filter>> filterChainMapEntry : filterChainProxyBeanEntry.getValue()
                    .getFilterChainMap().entrySet()) {
                final List<Filter> filterList = filterChainMapEntry.getValue();
                if (filterList.size() > 0) {
                    for (final Filter filter : filterList) {
                        if (filter instanceof SecurityContextPersistenceFilter) {
                            logger.info(
                                    "Found SecurityContextPersistenceFilter, mapped to URL '{}' in the FilterChainProxy bean named '{}', setting its securityContextRepository to the null implementation to disable caching of authentication",
                                    filterChainMapEntry.getKey(), filterChainProxyBeanEntry.getKey());
                            ((SecurityContextPersistenceFilter) filter).setSecurityContextRepository(
                             new NullSpringSecurityContextRepository());
                        }
                    }
                }

            }
        }
    }
}

Quoi qu'il en soit, à la solution qui fonctionne réellement, bien que très hackish. Utilisez simplement un Filterqui supprime l'entrée de session HttpSessionSecurityContextRepositoryrecherchée quand il fait son truc:

public class SpringSecuritySessionDeletingFilter extends GenericFilterBean implements Filter {

    @Override
    public void doFilter(final ServletRequest request_, final ServletResponse response_, final FilterChain chain_)
            throws IOException, ServletException {
        final HttpServletRequest servletRequest = (HttpServletRequest) request_;
        final HttpSession session = servletRequest.getSession();
        if (session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY) != null) {
            session.removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
        }

        chain_.doFilter(request_, response_);
    }
}

Puis dans la configuration:

<bean id="springSecuritySessionDeletingFilter"
    class="SpringSecuritySessionDeletingFilter" />

<sec:http auto-config="false" create-session="never"
    entry-point-ref="authEntryPoint">
    <sec:intercept-url pattern="/**"
        access="IS_AUTHENTICATED_REMEMBERED" />
    <sec:intercept-url pattern="/static/**" filters="none" />
    <sec:custom-filter ref="myLoginFilterChain"
        position="FORM_LOGIN_FILTER" />

    <sec:custom-filter ref="springSecuritySessionDeletingFilter"
        before="SECURITY_CONTEXT_FILTER" />
</sec:http>
Jeff Evans
la source
Neuf ans plus tard, c'est toujours la bonne réponse. Nous pouvons maintenant utiliser la configuration Java au lieu de XML. J'ai ajouté le filtre personnalisé dans mon WebSecurityConfigurerAdapteravec " http.addFilterBefore(new SpringSecuritySessionDeletingFilter(), SecurityContextPersistenceFilter.class)"
workerjoe
3

Juste un petit mot: c'est "create-session" plutôt que "create-sessions"

create-session

Contrôle l'empressement avec lequel une session HTTP est créée.

S'il n'est pas défini, la valeur par défaut est "ifRequired". Les autres options sont «toujours» et «jamais».

Le paramètre de cet attribut affecte les propriétés allowSessionCreation et forceEagerSessionCreation de HttpSessionContextIntegrationFilter. allowSessionCreation sera toujours vrai à moins que cet attribut ne soit défini sur "jamais". forceEagerSessionCreation est "faux" sauf s'il est défini sur "toujours".

Ainsi, la configuration par défaut autorise la création de session mais ne la force pas. L'exception est si le contrôle de session simultanée est activé, lorsque forceEagerSessionCreation sera défini sur true, quel que soit le paramètre ici. L'utilisation de "jamais" provoquerait alors une exception lors de l'initialisation de HttpSessionContextIntegrationFilter.

Pour des détails spécifiques sur l'utilisation de la session, il existe une bonne documentation dans le javadoc HttpSessionSecurityContextRepository.

Jon Vaughan
la source
Ce sont toutes d'excellentes réponses, mais je me suis cogné la tête contre le mur en essayant de comprendre comment y parvenir en utilisant l'élément de configuration <http>. Même avec auto-config=false, vous ne pouvez apparemment pas remplacer ce qui est en SECURITY_CONTEXT_FILTERposition par le vôtre. J'ai essayé de le désactiver avec un ApplicationContextAwarebean (en utilisant la réflexion pour forcer l' securityContextRepositoryimplémentation nulle SessionManagementFilter) mais pas de dés. Et malheureusement, je ne peux pas passer à Spring-Security 3.1 an qui fournirait create-session=stateless.
Jeff Evans
Veuillez visiter ce site, toujours informatif. J'espère que cela vous aidera ainsi que les autres " baeldung.com/spring-security-session " • toujours - une session sera toujours créée si elle n'existe pas déjà • ifRequired - une session ne sera créée que si nécessaire (par défaut) • jamais - le framework ne créera jamais une session lui-même mais il en utilisera une si elle existe déjà • sans état - aucune session ne sera créée ou utilisée par Spring Security
Java_Fire_Within