【问题标题】:How can I use Spring Security without sessions?如何在没有会话的情况下使用 Spring Security?
【发布时间】:2011-01-31 01:36:57
【问题描述】:

我正在使用 Spring Security 构建一个 Web 应用程序,该应用程序将运行在 Amazon EC2 上并使用 Amazon 的弹性负载均衡器。不幸的是,ELB 不支持粘性会话,所以我需要确保我的应用程序在没有会话的情况下正常工作。

到目前为止,我已经设置了 RememberMeServices 以通过 cookie 分配令牌,这工作正常,但我希望 cookie 在浏览器会话中过期(例如,当浏览器关闭时)。

我不得不想象我不是第一个想要在没有会话的情况下使用 Spring Security 的人......有什么建议吗?

【问题讨论】:

    标签: spring spring-security load-balancing amazon-ec2


    【解决方案1】:

    在带有Java Config的Spring Security 3中,你可以使用HttpSecurity.sessionManagement()

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    

    【讨论】:

    • 这是 Java 配置的正确答案,反映了 @sappenin 在对已接受答案的评论中为 xml 配置正确陈述的内容。我们使用这种方法,实际上我们的应用程序是无会话的。
    • 这有副作用。 Tomcat 容器会将“;jsessionid=...”附加到对图像、样式表等的请求中,因为 Tomcat 不喜欢无状态,然后 Spring Security 将在第一次加载时阻止这些资产,因为“URL 包含潜在的恶意字符串';'"。
    • @workerjoe 那么,你想通过这个java配置说什么,会话不是由spring security而不是tomcat创建的?
    • @VishwasAtrey 在我的理解(可能是错误的)中,Tomcat 创建并维护会话。 Spring 利用它们,添加自己的数据。正如我上面提到的,我试图制作一个无状态的 Web 应用程序,但它不起作用。请参阅this answer to my own question 了解更多信息。
    【解决方案2】:

    在 Spring Securitiy 3.0 中似乎更容易了。如果您使用命名空间配置,您可以简单地执行以下操作:

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

    或者您可以将 SecurityContextRepository 配置为 null,并且不会以这种方式保存任何内容 as well

    【讨论】:

    • 这并没有像我想象的那样工作。相反,下面有一条评论区分“从不”和“无状态”。使用“从不”,我的应用程序仍在创建会话。使用“无状态”,我的应用程序实际上是无状态的,我不需要实现其他答案中提到的任何覆盖。在此处查看 JIRA 问题:jira.springsource.org/browse/SEC-1424
    【解决方案3】:

    我们今天在同一问题上工作了 4-5 个小时(将自定义 SecurityContextRepository 注入到 SecurityContextPersistenceFilter)。最后,我们想通了。 首先,在 Spring Security ref 的第 8.3 节中。 doc,有一个 SecurityContextPersistenceFilter bean定义

    <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>
    

    在这个定义之后,有这样的解释: “或者,您可以提供 SecurityContextRepository 接口的空实现,这将阻止存储安全上下文,即使在请求期间已经创建了会话。”

    我们需要将自定义的 SecurityContextRepository 注入到 SecurityContextPersistenceFilter 中。所以我们简单地用我们的自定义 impl 更改了上面的 bean 定义,并将其放入安全上下文中。

    当我们运行应用程序时,我们跟踪日志并发现 SecurityContextPersistenceFilter 没有使用我们的自定义 impl,而是使用了 HttpSessionSecurityContextRepository。

    在我们尝试了其他一些事情之后,我们发现我们必须为我们的自定义 SecurityContextRepository impl 提供“http”命名空间的“security-context-repository-ref”属性。如果您使用“http”命名空间并想注入自己的 SecurityContextRepository impl,请尝试“security-context-repository-ref”属性。

    使用“http”命名空间时,会忽略单独的 SecurityContextPersistenceFilter 定义。正如我在上面复制的,参考文档。没有说明。

    如果我误解了事情,请纠正我。

    【讨论】:

    • 谢谢,这是很有价值的信息。我将在我的应用程序中尝试一下。
    • 谢谢,这就是我在 spring 3.0 中所需要的
    • 你说的很准确,http 命名空间不允许自定义 SecurityContextPersistenceFilter,我花了几个小时调试才弄明白
    • 非常感谢您发布此消息!我正要撕掉我的头发。我想知道为什么不推荐使用 SecurityContextPersistenceFilter 的 setSecurityContextRepository 方法(文档说使用构造函数注入,这也不正确)。
    【解决方案4】:

    看看SecurityContextPersistenceFilter 类。它定义了SecurityContextHolder 的填充方式。默认情况下,它使用HttpSessionSecurityContextRepository 在 http 会话中存储安全上下文。

    我用自定义SecurityContextRepository 很容易地实现了这个机制。

    请参阅下面的securityContext.xml

    <?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,您能否提供更多关于您的安全上下文存储库实施的详细信息?
    • 类 TokenSecurityContextRepository 包含 HashMap contextMap。在 loadContext() 方法中检查是否存在由 requestParameter sid、cookie、自定义 requestHeader 或上述任何组合传递的会话哈希码的 SecurityContext。如果无法解析上下文,则返回 SecurityContextHolder.createEmptyContext()。方法 saveContext 将解析的上下文放入 contextMap。
    【解决方案5】:

    实际上create-session="never" 并不意味着完全无国籍。在 Spring Security 问题管理中有 an issue

    【讨论】:

      【解决方案6】:

      编辑:从 Spring Security 3.1 开始,有一个 STATELESS 选项可以用来代替所有这些。查看其他答案。原始答案保留在下面以供后代使用。

      在与此答案中发布的众多解决方案作斗争之后,为了尝试在使用 &lt;http&gt; 命名空间配置时使某些东西正常工作,我终于找到了一种真正适用于我的用例的方法。我实际上并不要求 Spring Security 不启动会话(因为我在应用程序的其他部分使用会话),只是它根本不“记住”会话中的身份验证(应该重新检查每个请求)。

      首先,我无法弄清楚如何执行上述“空实现”技术。目前尚不清楚您是否应该将 securityContextRepository 设置为 null 或无操作实现。前者不起作用,因为NullPointerExceptionSecurityContextPersistenceFilter.doFilter() 中被抛出。至于无操作实现,我尝试以我能想象到的最简单的方式实现:

      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;
          }
      
      }
      

      这在我的应用程序中不起作用,因为 ClassCastExceptionresponse_ 类型有关。

      即使假设我确实设法找到了一个可行的实现(通过简单地不在会话中存储上下文),仍然存在如何将其注入由&lt;http&gt; 配置构建的过滤器的问题。根据docs,您不能简单地更换SECURITY_CONTEXT_FILTER 位置的过滤器。我发现挂钩在幕后创建的SecurityContextPersistenceFilter 的唯一方法是编写一个丑陋的ApplicationContextAware bean:

      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());
                              }
                          }
                      }
      
                  }
              }
          }
      }
      

      无论如何,对于实际有效的解决方案,尽管非常老套。只需使用Filter 删除HttpSessionSecurityContextRepository 在执行操作时查找的会话条目:

      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_);
          }
      }
      

      然后在配置中:

      <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>
      

      【讨论】:

      • 九年后,这仍然是正确的答案。现在我们可以使用 Java 配置而不是 XML。我用“http.addFilterBefore(new SpringSecuritySessionDeletingFilter(), SecurityContextPersistenceFilter.class)”在WebSecurityConfigurerAdapter 中添加了自定义过滤器
      • 如果您按照另一个答案中的描述使用 SessionCreationPolicy.STATELESS,则没有必要这样做。你一定有其他事情发生。
      • STATELESS 似乎已在 3.1 中添加。在编写此答案时,最新发布的版本是 3.0。这样就可以解释了。
      • 感谢@JeffEvans,SpringSecuritySessionDeletingFilter 为我节省了很多时间。在某些情况下我需要无状态行为时遇到问题,而在另一些情况下则不需要
      【解决方案7】:

      简单说明一下:它是“create-session”而不是“create-sessions”

      create-session

      控制创建 HTTP 会话的急切程度。

      如果未设置,则默认为“ifRequired”。其他选项是“总是”和“从不”。

      此属性的设置会影响 HttpSessionContextIntegrationFilter 的 allowSessionCreation 和 forceEagerSessionCreation 属性。除非此属性设置为“从不”,否则 allowSessionCreation 将始终为真。 forceEagerSessionCreation 为“false”,除非它设置为“always”。

      所以默认配置允许创建会话但不强制。例外情况是,如果启用了并发会话控制,则​​当 forceEagerSessionCreation 设置为 true 时,无论此处的设置如何。使用“never”会在 HttpSessionContextIntegrationFilter 初始化期间导致异常。

      关于会话使用的具体细节,HttpSessionSecurityContextRepository javadoc中有一些很好的文档。

      【讨论】:

      • 这些都是很好的答案,但我一直在拼命想弄清楚在使用 配置元素时如何实现这一点。即使使用auto-config=false,您显然也无法用您自己的替换SECURITY_CONTEXT_FILTER 位置中的内容。我一直在尝试用一些ApplicationContextAware bean 禁用它(使用反射将securityContextRepository 强制为SessionManagementFilter 中的空实现)但没有骰子。可悲的是,我无法切换到提供create-session=stateless 的 spring-security 3.1 年。
      • 请访问此站点,始终提供信息。希望这对您和其他人也有帮助 "baeldung.com/spring-security-session" • always – 如果会话尚不存在,则始终创建会话 • ifRequired – 仅在需要时创建会话(默认) • never – 框架永远不会自己创建一个会话,但如果它已经存在,它将使用一个 • 无状态——Spring Security 不会创建或使用任何会话
      【解决方案8】:

      现在 ELB 支持粘性会话,我想从 2016 年开始。 但也可以将会话存储在 Redis 中。

      【讨论】:

      猜你喜欢
      • 2023-02-05
      • 2011-09-16
      • 2014-09-15
      • 1970-01-01
      • 2022-06-13
      • 1970-01-01
      • 1970-01-01
      • 2015-11-05
      • 2020-12-23
      相关资源
      最近更新 更多