10. Web 应用安全

大多数 Spring Security 用户将在使用 HTTP 和 Servlet API 的应用程序中使用该框架。在这一部分中,我们将研究 Spring Security 如何为应用程序的 Web 层提供身份验证和访问控制功能。我们将查看命名空间的外观,并查看实际组装了哪些类和接口以提供 Web 层安全性。在某些情况下,有必要使用传统的 bean 配置来提供对配置的完全控制,因此我们还将看到如何直接在没有名称空间的情况下配置这些类。

10.1 安全筛选器链

Spring Security 的 Web 基础结构完全基于标准 Servlet 过滤器。它在内部不使用 servlet 或任何其他基于 servlet 的框架(例如 Spring MVC),因此它与任何特定的 Web 技术都没有牢固的链接。它处理HttpServletRequestHttpServletResponse,并且不关心请求来自浏览器,Web 服务 Client 端,HttpInvoker还是 AJAX 应用程序。

Spring Security 在内部维护一个过滤器链,其中每个过滤器都有特定的职责,并且根据需要的服务将过滤器添加到配置中或从配置中删除。过滤器的 Sequences 很重要,因为它们之间存在依赖性。如果您一直在使用namespace configuration,那么将自动为您配置过滤器,而不必显式定义任何 Spring Bean,但是有时您可能希望完全控制安全过滤器链,这可能是因为您使用的功能命名空间中不受支持,或者您正在使用自己的自定义版本的类。

10.1.1 DelegatingFilterProxy

使用 servlet 过滤器时,显然需要在web.xml中声明它们,否则 servlet 容器将忽略它们。在 Spring Security 中,过滤器类也是在应用程序上下文中定义的 Spring Bean,因此能够利用 Spring 丰富的依赖注入机制和生命周期接口。 Spring 的DelegatingFilterProxy提供web.xml和应用程序上下文之间的链接。

使用DelegatingFilterProxy时,您将在web.xml文件中看到以下内容:

<filter>
<filter-name>myFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

请注意,过滤器实际上是DelegatingFilterProxy,而不是实际上将实现过滤器逻辑的类。 DelegatingFilterProxy的工作是将Filter的方法委托给从 Spring 应用程序上下文获得的 bean。这使 Bean 可以从 Spring Web 应用程序上下文生命周期支持和配置灵 Active 中受益。 Bean 必须实现javax.servlet.Filter,并且必须与filter-name元素中的名称相同。阅读DelegatingFilterProxy的 Javadoc 了解更多信息

10.1.2 FilterChainProxy

Spring Security 的 Web 基础结构只能通过委派给FilterChainProxy的实例来使用。安全过滤器不应自己使用。从理论上讲,您可以在应用程序上下文文件中声明所需的每个 Spring Security 过滤器 bean,并为每个过滤器在web.xml上添加一个对应的DelegatingFilterProxy条目,以确保正确排序,但这会很麻烦并且会使web.xml文件杂乱无章如果您有很多过滤器,则速度很快。 FilterChainProxy让我们向web.xml添加一个条目,并完全处理用于 ManagementWeb 安全 bean 的应用程序上下文文件。就像上面的示例一样,它使用DelegatingFilterProxy进行接线,但是将filter-name设置为 bean 名称“ filterChainProxy”。然后在应用程序上下文中使用相同的 bean 名称声明过滤器链。这是一个例子:

<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg>
    <list>
    <sec:filter-chain pattern="/restful/**" filters="
        securityContextPersistenceFilterWithASCFalse,
        basicAuthenticationFilter,
        exceptionTranslationFilter,
        filterSecurityInterceptor" />
    <sec:filter-chain pattern="/**" filters="
        securityContextPersistenceFilterWithASCTrue,
        formLoginFilter,
        exceptionTranslationFilter,
        filterSecurityInterceptor" />
    </list>
</constructor-arg>
</bean>

为了方便起见,使用命名空间元素filter-chain来设置应用程序内所需的安全过滤器链。 [6]。它将特定的 URL 模式 Map 到由filters元素中指定的 bean 名称构建的过滤器列表,并将它们组合为SecurityFilterChain类型的 bean。 pattern属性采用“Ant 路径”,最具体的 URI 应该首先出现在[7]上。在运行时FilterChainProxy将找到与当前 Web 请求匹配的第一个 URI 模式,并且filters属性指定的过滤器 bean 列表将应用于该请求。筛选器将按照定义的 Sequences 调用,因此您可以完全控制应用于特定 URL 的筛选器链。

您可能已经注意到,我们在过滤器链中声明了两个SecurityContextPersistenceFilter(ASCallowSessionCreation的缩写,SecurityContextPersistenceFilter的属性)。由于 Web 服务永远不会在将来的请求中提供jsessionid,因此为此类用户代理创建HttpSession会很浪费。如果您有需要最大可伸缩性的大容量应用程序,则建议您使用上面显示的方法。对于较小的应用程序,使用单个SecurityContextPersistenceFilter(其默认allowSessionCreation作为true)可能就足够了。

请注意,FilterChainProxy不会在配置它的过滤器上调用标准过滤器生命周期方法。我们建议您像其他任何 Spring bean 一样,使用 Spring 的应用程序上下文生命周期接口作为替代。

当我们研究如何使用namespace configuration设置网络安全性时,我们使用了名称为“ springSecurityFilterChain”的DelegatingFilterProxy。现在,您应该能够看到这是名称空间创建的FilterChainProxy的名称。

绕过过滤器链

您可以使用属性filters = "none"代替提供过滤器 bean 列表。这将完全省略来自安全过滤器链的请求模式。请注意,与此路径匹配的所有内容都将不应用身份验证或授权服务,并且可以自由访问。如果要在请求期间使用SecurityContext内容的内容,则它必须已通过安全过滤器链。否则,将不会填充SecurityContextHolder,并且内容将为 null。

10.1.3 过滤器 Order

链中定义过滤器的 Sequences 非常重要。无论您实际使用哪个过滤器,其 Sequences 都应如下:

  • ChannelProcessingFilter,因为它可能需要重定向到其他协议

  • SecurityContextPersistenceFilter,因此可以在 Web 请求开始时在SecurityContextHolder中设置SecurityContext,并且在 Web 请求结束时(对下一个 Web 请求可用)对SecurityContext所做的任何更改都可以复制到HttpSession中。

  • ConcurrentSessionFilter,因为它使用SecurityContextHolder功能并且需要更新SessionRegistry以反映来自委托人的持续请求

  • 认证处理机制UsernamePasswordAuthenticationFilterCasAuthenticationFilterBasicAuthenticationFilter等-以便可以将SecurityContextHolder修改为包含有效的Authentication请求令牌

  • SecurityContextHolderAwareRequestFilter,如果您使用它来将支持 Spring Security 的HttpServletRequestWrapper安装到 servlet 容器中

  • JaasApiIntegrationFilter,如果SecurityContextHolder中有JaasAuthenticationToken,则它将FilterChain作为JaasAuthenticationToken中的Subject处理。

  • RememberMeAuthenticationFilter,因此,如果没有较早的身份验证处理机制更新SecurityContextHolder,并且请求提出一个 cookie 来启用“记住我”服务,则会在此处放置一个合适的记住Authentication对象

  • AnonymousAuthenticationFilter,因此,如果没有较早的身份验证处理机制更新SecurityContextHolder,则将在其中放置一个匿名Authentication对象

  • ExceptionTranslationFilter,以捕获任何 Spring Security 异常,以便可以返回 HTTP 错误响应或启动适当的AuthenticationEntryPoint

  • FilterSecurityInterceptor,以保护 Web URI 并在拒绝访问时引发异常

10.1.4 请求匹配和 HttpFirewall

Spring Security 在多个区域中针对传入的请求对您定义的模式进行测试,以便确定应如何处理请求。当FilterChainProxy决定请求应通过哪个过滤器链时,以及FilterSecurityInterceptor决定哪些安全性约束适用于请求时,就会发生这种情况。在针对您定义的模式进行测试时,了解该机制是什么以及使用哪个 URL 值很重要。

Servlet 规范为HttpServletRequest定义了几个属性,可通过 getter 方法访问这些属性,我们可能希望将其匹配。这些是contextPathservletPathpathInfoqueryString。 Spring Security 只对保护应用程序中的路径感兴趣,因此contextPath被忽略。不幸的是,该 servlet 规范没有确切定义servletPathpathInfo的值将包含特定请求 URI 的内容。例如,URL 的每个路径段都可以包含RFC 2396 [8]中定义的参数。规范没有明确规定是否将这些值包含在servletPathpathInfo值中,并且行为在不同的 servlet 容器之间有所不同。存在以下危险:当将应用程序部署在不从这些值中剥离路径参数的容器中时,攻击者可能会将其添加到请求的 URL 中,从而导致模式匹配意外成功或失败。 [9]。传入 URL 中的其他变体也是可能的。例如,它可能包含路径遍历序列(如/../)或多个正斜杠(//),这也可能导致模式匹配失败。一些容器在执行 servletMap 之前将它们标准化,但其他容器则没有。为了防止此类问题,FilterChainProxy使用HttpFirewall策略检查并包装请求。默认情况下,未规范的请求将自动被拒绝,并且出于匹配目的,将删除路径参数和重复的斜杠。 [10]。因此,必须使用FilterChainProxy来 Management 安全过滤器链。请注意servletPathpathInfo值是由容器解码的,因此您的应用程序不应具有任何包含分号的有效路径,因为出于匹配目的将删除这些部分。

如上所述,默认策略是使用 Ant 样式的路径进行匹配,这对于大多数用户而言可能是最佳选择。该策略在类AntPathRequestMatcher中实现,该类使用 Spring 的AntPathMatcher对连接的servletPathpathInfo执行模式的大小写不敏感匹配,而忽略queryString

如果由于某种原因需要更强大的匹配策略,则可以使用正则表达式。该策略的实现为RegexRequestMatcher。有关更多信息,请参见 Javadoc。

实际上,我们建议您在服务层使用方法安全性来控制对应用程序的访问,并且不要完全依赖于在 Web 应用程序级别定义的安全性约束的使用。 URL 会发生变化,很难考虑到应用程序可能支持的所有可能的 URL 以及如何处理请求。您应该尝试限制自己使用一些简单易懂的简单 Ant 路径。始终尝试使用“默认拒绝”方法,在此方法中,您最后定义了一个全包通配符(/ )并拒绝访问。

在服务层定义的安全性更健壮,更难绕开,因此您应始终利用 Spring Security 的方法安全性选项。

HttpFirewall还通过拒绝 HTTP 响应 Headers 中的换行符来防止HTTP 响应拆分

默认情况下使用StrictHttpFirewall。此实现拒绝看起来是恶意的请求。如果对您的要求过于严格,则可以自定义拒绝哪些类型的请求。但是,重要的是要知道这样做可以使您的应用程序容易受到攻击。例如,如果您希望利用 Spring MVC 的矩阵变量,可以在 XML 中使用以下配置:

<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowSemicolon="true"/>

<http-firewall ref="httpFirewall"/>

使用 Java 配置,可以通过公开StrictHttpFirewall bean 实现相同的目的。

@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}

StrictHttpFirewall提供了有效的 HTTP 方法的白名单,这些方法可用来防止跨站点跟踪(XST)HTTP 动词篡改。默认的有效方法是“ DELETE”,“ GET”,“ HEAD”,“ OPTIONS”,“ PATCH”,“ POST”和“ PUT”。如果您的应用程序需要修改有效方法,则可以配置自定义StrictHttpFirewall bean。例如,以下内容仅允许 HTTP“ GET”和“ POST”方法:

<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowedHttpMethods="GET,HEAD"/>

<http-firewall ref="httpFirewall"/>

使用 Java 配置,可以通过公开StrictHttpFirewall bean 实现相同的目的。

@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
    return firewall;
}

Tip

如果您使用的是new MockHttpServletRequest(),则当前会创建一个 HTTP 方法作为空字符串“”。这是无效的 HTTP 方法,将被 Spring Security 拒绝。您可以通过将其替换为new MockHttpServletRequest("GET", "")来解决此问题。请参阅SPR_16851,以寻求解决此问题的方法。

如果必须允许任何 HTTP 方法(不推荐),则可以使用StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)。这将完全禁用 HTTP 方法的验证。

10.1.5 与其他基于过滤器的框架一起使用

如果您使用的是基于过滤器的其他框架,则需要确保首先使用 Spring Security 过滤器。这样可以及时填充SecurityContextHolder,以供其他过滤器使用。例如,使用 SiteMesh 装饰网页或使用过滤器处理其请求的 Wicket 之类的网络框架。

10.1.6 高级命名空间配置

正如我们在名称空间一章的前面所看到的,可以使用多个http元素为不同的 URL 模式定义不同的安全配置。每个元素都会在内部FilterChainProxy和应该 Map 到其的 URL 模式内创建一个过滤器链。元素将按照声明的 Sequences 添加,因此最具体的模式必须再次声明。这是另一个示例,与上述情况类似,该应用程序既支持 StatelessRESTful API,也支持用户使用表单登录的普通 Web 应用程序。

<!-- Stateless RESTful service using Basic authentication -->
<http pattern="/restful/**" create-session="stateless">
<intercept-url pattern='/**' access="hasRole('REMOTE')" />
<http-basic />
</http>

<!-- Empty filter chain for the login page -->
<http pattern="/login.htm*" security="none"/>

<!-- Additional filter chain for normal users, matching all other requests -->
<http>
<intercept-url pattern='/**' access="hasRole('USER')" />
<form-login login-page='/login.htm' default-target-url="/home.htm"/>
<logout />
</http>

10.2 核心安全过滤器

有一些关键过滤器将始终在使用 Spring Security 的 Web 应用程序中使用,因此我们将首先研究这些过滤器及其支持的类和接口。我们不会涵盖所有功能,因此,如果您想获得完整的图片,请务必查看 Javadoc。

10.2.1 FilterSecurityInterceptor

在讨论一般的访问控制时,我们已经简短地看到了FilterSecurityInterceptor,并且已经将其与<intercept-url>元素组合以在内部进行配置的名称空间一起使用。现在,我们将看到如何显式配置它与FilterChainProxy及其配套过滤器ExceptionTranslationFilter一起使用。一个典型的配置示例如下所示:

<bean id="filterSecurityInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="securityMetadataSource">
    <security:filter-security-metadata-source>
    <security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>
    <security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
    </security:filter-security-metadata-source>
</property>
</bean>

FilterSecurityInterceptor负责处理 HTTP 资源的安全性。它需要引用AuthenticationManagerAccessDecisionManager。它还提供了适用于不同 HTTP URL 请求的配置属性。请参考技术简介中的这些的原始讨论

可以通过两种方式使用配置属性来配置FilterSecurityInterceptor。如上所示,第一个使用的是<filter-security-metadata-source>名称空间元素。这类似于命名空间章节中的<http>元素,但<intercept-url>子元素仅使用patternaccess属性。逗号用于界定适用于每个 HTTP URL 的不同配置属性。第二种选择是编写自己的SecurityMetadataSource,但这超出了本文档的范围。与使用的方法无关,SecurityMetadataSource负责返回List<ConfigAttribute>,该List<ConfigAttribute>包含与单个安全 HTTP URL 相关联的所有配置属性。

应该注意的是FilterSecurityInterceptor.setSecurityMetadataSource()方法实际上期望一个FilterInvocationSecurityMetadataSource的实例。这是SecurityMetadataSource子类的标记接口。它仅表示SecurityMetadataSource理解FilterInvocation。为了简单起见,我们将 continue 将FilterInvocationSecurityMetadataSource称为SecurityMetadataSource,因为这种区别与大多数用户无关。

由名称空间语法创建的SecurityMetadataSource通过将请求 URL 与已配置的pattern属性进行匹配来获取特定FilterInvocation的配置属性。这与命名空间配置的行为相同。默认是将所有表达式视为 Apache Ant 路径,对于更复杂的情况也支持正则表达式。 request-matcher属性用于指定要使用的模式的类型。无法在同一定义中混合使用表达式语法。例如,使用正则表达式而不是 Ant 路径的先前配置将编写如下:

<bean id="filterInvocationInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="runAsManager" ref="runAsManager"/>
<property name="securityMetadataSource">
    <security:filter-security-metadata-source request-matcher="regex">
    <security:intercept-url pattern="\A/secure/super/.*\Z" access="ROLE_WE_DONT_HAVE"/>
    <security:intercept-url pattern="\A/secure/.*\" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
    </security:filter-security-metadata-source>
</property>
</bean>

模式始终按照定义的 Sequences 进行评估。因此,重要的是,在列表中将比不那么具体的模式定义更具体的模式更高。这在上面的示例中得到了反映,在此示例中,更具体的/secure/super/模式看上去比不那么具体的/secure/模式更高。如果它们相反,则/secure/模式将始终匹配,并且将永远不会评估/secure/super/模式。

10.2.2 ExceptionTranslationFilter

ExceptionTranslationFilter位于安全过滤器堆栈中的FilterSecurityInterceptor上方。它本身并没有执行任何实际的安全性强制措施,而是处理安全性拦截器引发的异常并提供适当的 HTTP 响应。

<bean id="exceptionTranslationFilter"
class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
<property name="accessDeniedHandler" ref="accessDeniedHandler"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>

<bean id="accessDeniedHandler"
    class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.htm"/>
</bean>

AuthenticationEntryPoint

如果用户请求安全的 HTTP 资源但未通过身份验证,则将调用AuthenticationEntryPoint。安全拦截器将在调用堆栈的更下方抛出适当的AuthenticationExceptionAccessDeniedException,从而触发入口点上的commence方法。这样做的工作是向用户呈现适当的响应,以便可以开始身份验证。我们在这里使用的是LoginUrlAuthenticationEntryPoint,它将请求重定向到另一个 URL(通常是登录页面)。实际使用的实现将取决于您要在应用程序中使用的身份验证机制。

AccessDeniedHandler

如果用户已经通过身份验证并尝试访问受保护的资源,会发生什么情况?在正常使用情况下,不应发生这种情况,因为应用程序工作流应限于用户有权访问的操作。例如,没有 Management 员角色的用户可能会隐藏指向 Management 页面的 HTML 链接。但是,您不能依靠隐藏链接来确保安全性,因为用户总是有可能直接 ImportingURL 来尝试绕过限制。或者,他们可能会修改 RESTful URL 来更改某些参数值。您的应用程序必须受到保护以防出现这些情况,否则肯定是不安全的。通常,您将使用简单的 Web 层安全性将约束应用于基本 URL,并在服务层接口上使用基于方法的更特定的安全性来 true 确定允许的内容。

如果抛出AccessDeniedException并且用户已通过身份验证,则意味着已尝试操作该用户没有足够权限的操作。在这种情况下,ExceptionTranslationFilter将调用第二种策略AccessDeniedHandler。默认情况下,使用AccessDeniedHandlerImpl,它仅向 Client 端发送 403(禁止访问)响应。或者,您可以显式配置实例(如上例所示),并设置错误页面 URL,它将请求转发至[11]。这可以是简单的“访问被拒绝”页面,例如 JSP,也可以是更复杂的处理程序,例如 MVC 控制器。当然,您可以自己实现接口并使用自己的实现。

使用命名空间配置应用程序时,也可以提供自定义的AccessDeniedHandler。有关更多详细信息,请参见命名空间附录

SavedRequest 和 RequestCache 接口

ExceptionTranslationFilter职责的另一个职责是在调用AuthenticationEntryPoint之前保存当前请求。这允许在用户认证之后恢复请求(请参见web authentication的先前概述)。一个典型的示例是用户使用表单登录,然后通过默认的SavedRequestAwareAuthenticationSuccessHandler(请参阅below)重定向到原始 URL。

RequestCache封装了存储和检索HttpServletRequest实例所需的功能。默认情况下使用HttpSessionRequestCache,它将请求存储在HttpSession中。 RequestCacheFilter的工作是当用户重定向到原始 URL 时从缓存中实际恢复已保存的请求。

通常情况下,您不需要修改任何此功能,但是保存请求处理是一种“尽力而为”的方法,并且在某些情况下默认配置无法处理。这些接口的使用使其可以从 Spring Security 3.0 开始完全插入。

10.2.3 SecurityContextPersistenceFilter

我们已在Technical Overview一章中介绍了此至关重要的过滤器的用途,因此您现在可能希望重新阅读该部分。首先让我们看一下如何配置它与FilterChainProxy结合使用。基本配置仅需要 bean 本身

<bean id="securityContextPersistenceFilter"
class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>

正如我们之前看到的,此过滤器有两个主要任务。它负责在 HTTP 请求之间存储SecurityContext内容,并在请求完成时清除SecurityContextHolder。清除存储上下文的ThreadLocal是必不可少的,因为否则可能会将线程替换为 servlet 容器的线程池,同时仍附加特定用户的安全上下文。然后可能在以后的阶段使用此线程,并以错误的凭据执行操作。

SecurityContextRepository

从 Spring Security 3.0 开始,现在将安全性上下文的加载和存储工作委托给一个单独的策略接口:

public interface SecurityContextRepository {

SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);

void saveContext(SecurityContext context, HttpServletRequest request,
        HttpServletResponse response);
}

HttpRequestResponseHolder只是传入请求和响应对象的容器,允许实现将其替换为包装器类。返回的内容将传递到过滤器链。

默认实现是HttpSessionSecurityContextRepository,它将安全性上下文存储为HttpSession属性[12]。此实现最重要的配置参数是allowSessionCreation属性,该属性默认为true,从而允许该类在需要一个会话来存储经过身份验证的用户的安全上下文时创建会话(除非进行了身份验证,否则它不会创建一个会话)位置以及安全上下文的内容已更改)。如果您不想创建会话,则可以将此属性设置为false

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

或者,您可以提供NullSecurityContextRepositorynull object实现的实例,即使在请求期间已经创建了会话,也将阻止存储安全上下文。

10.2.4 UsernamePasswordAuthenticationFilter

现在,我们已经看到了 Spring Security Web 配置中始终存在的三个主要过滤器。这也是由名称空间<http>元素自动创建的三个,不能用替代项代替。现在唯一缺少的是一种实际的身份验证机制,该机制将允许用户进行身份验证。此过滤器是最常用的身份验证过滤器,也是最常定制的[13]。它还提供了命名空间中<form-login>元素使用的实现。对其进行配置需要三个阶段。

  • 像上面一样,使用登录页面的 URL 配置LoginUrlAuthenticationEntryPoint,并将其设置在ExceptionTranslationFilter上。

  • 实现登录页面(使用 JSP 或 MVC 控制器)。

  • 在应用程序上下文中配置UsernamePasswordAuthenticationFilter的实例

  • 将滤 bean 添加到您的过滤链代理(确保您注意 Sequences)。

登录表单仅包含usernamepasswordImporting 字段,并发布到由过滤器监视的 URL(默认为/login)。基本过滤器配置如下所示:

<bean id="authenticationFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

身份验证成功和失败的应用程序流程

过滤器调用已配置的AuthenticationManager来处理每个身份验证请求。认证成功或认证失败后的目的地分别由AuthenticationSuccessHandlerAuthenticationFailureHandler策略接口控制。过滤器具有允许您设置这些属性的属性,因此您可以完全自定义行为[14]。提供了一些标准实现,例如SimpleUrlAuthenticationSuccessHandlerSavedRequestAwareAuthenticationSuccessHandlerSimpleUrlAuthenticationFailureHandlerExceptionMappingAuthenticationFailureHandlerDelegatingAuthenticationFailureHandler。查看这些类以及AbstractAuthenticationProcessingFilter的 Javadoc,以大致了解它们的工作方式和受支持的功能。

如果身份验证成功,则将结果Authentication对象放入SecurityContextHolder。然后将调用已配置的AuthenticationSuccessHandler来将用户重定向或转发到适当的目的地。默认情况下,使用SavedRequestAwareAuthenticationSuccessHandler,这意味着用户将被重定向到他们要求的原始目的地,然后才被要求登录。

Note

ExceptionTranslationFilter缓存用户发出的原始请求。当用户进行身份验证时,请求处理程序将使用此缓存的请求来获取原始 URL 并重定向到该 URL。然后,原始请求将被重建并用作替代。

如果身份验证失败,则将调用配置的AuthenticationFailureHandler

10.3 Servlet API 集成

本节描述了如何将 Spring Security 与 Servlet API 集成在一起。 servletapi-xml示例应用程序演示了每种方法的用法。

10.3.1 Servlet 2.5 集成

HttpServletRequest.getRemoteUser()

HttpServletRequest.getRemoteUser()将返回SecurityContextHolder.getContext().getAuthentication().getName()的结果,该结果通常是当前的用户名。如果要在应用程序中显示当前用户名,这将很有用。此外,检查此属性是否为 null 可以用来指示用户是否已通过身份验证或匿名。知道用户是否通过身份验证对于确定是否应显示某些 UI 元素很有用(即,仅在用户通过身份验证时才显示注销链接)。

HttpServletRequest.getUserPrincipal()

HttpServletRequest.getUserPrincipal()将返回SecurityContextHolder.getContext().getAuthentication()的结果。这意味着它是Authentication,当使用基于用户名和密码的身份验证时,通常是UsernamePasswordAuthenticationToken的实例。如果您需要有关用户的其他信息,这将很有用。例如,您可能已经创建了一个自定义UserDetailsService,该自定义UserDetailsService返回包含用户的名字和姓氏的自定义UserDetails。您可以通过以下方式获取此信息:

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();

Note

应当指出,在整个应用程序中执行如此多的逻辑通常是一种不好的做法。相反,应该将其集中化以减少 Spring Security 和 Servlet API 的任何耦合。

HttpServletRequest.isUserInRole(String)

HttpServletRequest.isUserInRole(String)将确定SecurityContextHolder.getContext().getAuthentication().getAuthorities()是否包含GrantedAuthority并将角色传递给isUserInRole(String)。通常,用户不应将“ ROLE_”前缀传递给此方法,因为它是自动添加的。例如,如果要确定当前用户是否具有权限“ ROLE_ADMIN”,则可以使用以下命令:

boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");

这对于确定是否应显示某些 UI 组件可能很有用。例如,仅当当前用户是 Management 员时,才可以显示 Management 员链接。

10.3.2 Servlet 3 集成

下一节描述了与 Spring Security 集成的 Servlet 3 方法。

HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse)

HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse)方法可用于确保对用户进行身份验证。如果未通过身份验证,则将使用配置的 AuthenticationEntryPoint 来请求用户进行身份验证(即重定向到登录页面)。

HttpServletRequest.login(String,String)

HttpServletRequest.login(String,String)方法可用于通过当前AuthenticationManager认证用户。例如,以下尝试使用用户名“ user”和密码“ password”进行身份验证:

try {
httpServletRequest.login("user","password");
} catch(ServletException e) {
// fail to authenticate
}

Note

如果您想让 Spring Security 处理失败的身份验证尝试,则不必捕获 ServletException。

HttpServletRequest.logout()

HttpServletRequest.logout()方法可用于注销当前用户。

通常,这意味着将清除 SecurityContextHolder,使 HttpSession 无效,将清除所有“记住我”身份验证,依此类推。但是,配置的 LogoutHandler 实现取决于您的 Spring Security 配置。重要的是要注意,在调用 HttpServletRequest.logout()之后,您仍然负责写出响应。通常,这将涉及重定向到欢迎页面。

AsyncContext.start(Runnable)

确保您的凭据将被传播到新线程的AsynchContext.start(Runnable)方法。使用 Spring Security 的并发支持,Spring Security 重写 AsyncContext.start(Runnable)以确保在处理 Runnable 时使用当前的 SecurityContext。例如,以下将输出当前用户的身份验证:

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {
    public void run() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        try {
            final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();
            asyncResponse.setStatus(HttpServletResponse.SC_OK);
            asyncResponse.getWriter().write(String.valueOf(authentication));
            async.complete();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
});

异步 Servlet 支持

如果您使用的是基于 Java 的配置,则可以开始使用。如果使用 XML 配置,则需要进行一些更新。第一步是确保已更新 web.xml,使其至少使用 3.0 模式,如下所示:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

</web-app>

接下来,您需要确保已设置 springSecurityFilterChain 来处理异步请求。

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
    org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

而已!现在,Spring Security 将确保您的 SecurityContext 也可以在异步请求中传播。

那么它是怎样工作的?如果您真的不感兴趣,请随时跳过本节的其余部分,否则请 continue 阅读。大部分内容都内置在 Servlet 规范中,但是 Spring Security 做了一些调整,以确保异步请求可以正常工作。在 Spring Security 3.2 之前,一旦提交 HttpServletResponse,就会自动保存 SecurityContextHolder 中的 SecurityContext。这可能会在异步环境中引起问题。例如,考虑以下内容:

httpServletRequest.startAsync();
new Thread("AsyncThread") {
    @Override
    public void run() {
        try {
            // Do work
            TimeUnit.SECONDS.sleep(1);

            // Write to and commit the httpServletResponse
            httpServletResponse.getOutputStream().flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}.start();

问题是 Spring Security 不知道该线程,因此不会将 SecurityContext 传播给它。这意味着当我们提交 HttpServletResponse 时,没有 SecuriytContext。当 Spring Security 在提交 HttpServletResponse 时自动保存 SecurityContext 时,它将丢失我们的登录用户。

从 3.2 版本开始,Spring Security 足够聪明,不再在调用 HttpServletRequest.startAsync()时自动保存 SecurityContext 来提交 HttpServletResponse。

10.3.3 Servlet 3.1 集成

下一节描述了与 Spring Security 集成的 Servlet 3.1 方法。

HttpServletRequest#changeSessionId()

HttpServletRequest.changeSessionId()是 Servlet 3.1 及更高版本中防范Session Fixation攻击的默认方法。

10.4 基本和摘要身份验证

基本身份验证和摘要身份验证是在 Web 应用程序中流行的替代身份验证机制。基本身份验证通常与 StatelessClient 端一起使用,StatelessClient 端会在每个请求中传递其凭据。将它与基于表单的身份验证结合使用非常普遍,在这种情况下,既可以通过基于浏览器的用户界面也可以通过 Web 服务来使用应用程序。但是,基本身份验证将密码以纯文本形式传输,因此,它只能在加密传输层(例如 HTTPS)上 true 使用。

10.4.1 BasicAuthenticationFilter

BasicAuthenticationFilter负责处理 HTTPHeaders 中显示的基本身份验证凭据。这可用于验证 Spring 远程协议(例如 Hessian 和 Burlap)以及普通浏览器用户代理(例如 Firefox 和 Internet Explorer)发出的调用。 RFC 1945 第 11 节定义了 ManagementHTTP 基本认证的标准,并且BasicAuthenticationFilter与此 RFC 一致。基本身份验证是一种有吸引力的身份验证方法,因为它已广泛部署在用户代理中,并且实现极为简单(它只是在 HTTPHeaders 中指定的 username:password 的 Base64 编码)。

Configuration

要实现 HTTP 基本身份验证,您需要在过滤器链中添加BasicAuthenticationFilter。应用程序上下文应包含BasicAuthenticationFilter及其必需的协作者:

<bean id="basicAuthenticationFilter"
class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
<property name="realmName" value="Name Of Your Realm"/>
</bean>

配置的AuthenticationManager处理每个身份验证请求。如果身份验证失败,将使用配置的AuthenticationEntryPoint重试身份验证过程。通常,您将过滤器与BasicAuthenticationEntryPoint结合使用,BasicAuthenticationEntryPoint返回带有适当 Headers 的 401 响应以重试 HTTP Basic 身份验证。如果身份验证成功,则照常将生成的Authentication对象放入SecurityContextHolder中。

如果身份验证事件成功,或者因为 HTTPHeaders 不包含受支持的身份验证请求而未尝试进行身份验证,则过滤器链将照常 continue。唯一的中断过滤器链的方法是验证失败并调用AuthenticationEntryPoint

10.4.2 DigestAuthenticationFilter

DigestAuthenticationFilter能够处理 HTTPHeaders 中提供的摘要身份验证凭据。摘要式身份验证试图解决基本身份验证的许多弱点,特别是通过确保凭据不会以明文形式通过网络发送来解决。许多用户代理都支持摘要式身份验证,包括 Mozilla Firefox 和 Internet Explorer。 HTTP 摘要认证的标准由 RFC 2617 定义,该标准更新了 RFC 2069 规定的摘要认证的早期版本。大多数用户代理实现 RFC2617.SpringSecurity 的DigestAuthenticationFilter与“ auth”保护质量(qop)兼容。由 RFC 2617 规定,它也提供与 RFC 2069 的向后兼容性。如果您需要使用未加密的 HTTP(即,没有 TLS/HTTPS)并且希望最大程度地提高身份验证过程的安全性,则摘要身份验证是一种更具吸引力的选择。实际上,如 RFC 2518 第 17.1 节所述,摘要式身份验证是 WebDAV 协议的强制性要求。

Note

您不应该在现代应用程序中使用 Digest,因为它不安全。最明显的问题是您必须以纯文本,加密或 MD5 格式存储密码。所有这些存储格式都被认为是不安全的。相反,您应该使用一种单向自适应密码哈希(即 bCrypt,PBKDF2,SCrypt 等)。

摘要式身份验证的核心是“一次性”。这是服务器生成的值。 Spring Security 的随机数采用以下格式:

base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime:   The date and time when the nonce expires, expressed in milliseconds
key:              A private key to prevent modification of the nonce token

DigestAuthenticatonEntryPoint具有指定用于生成随机数令牌的key的属性,以及nonceValiditySeconds用于确定到期时间的属性(默认值为 300,等于五分钟)。在随机数有效的情况下,摘要是通过串联各种字符串来计算的,这些字符串包括用户名,密码,随机数,所请求的 URI,Client 端生成的随机数(仅由用户代理生成每个请求的随机值),领域名称等,然后执行 MD5 哈希。服务器和用户代理都执行此摘要计算,如果它们不同意包含的值(例如密码),则会产生不同的哈希码。在 Spring Security 的实现中,如果服务器生成的随机数刚刚到期(但摘要有效),则DigestAuthenticationEntryPoint将发送"stale=true"Headers。这告诉用户代理无需打扰用户(因为密码和用户名等是正确的),而只需使用新的随机数重试即可。

nonceValiditySeconds参数DigestAuthenticationEntryPoint的适当值取决于您的应用程序。极其安全的应用程序应注意,可以使用拦截的身份验证 Headers 来模拟主体,直到达到随机数中所包含的expirationTime为止。这是选择适当设置时的关键原则,但是对于非常安全的应用程序,在最初的情况下不会在 TLS/HTTPS 上运行是不常见的。

由于摘要式身份验证的实施更为复杂,因此经常存在用户代理问题。例如,Internet Explorer 无法在同一会话中的后续请求上提供“不透明”令牌。因此,Spring Security 过滤器将所有状态信息封装到“ nonce”令牌中。在我们的测试中,Spring Security 的实现可与 Mozilla Firefox 和 Internet Explorer 可靠地配合,正确处理随机数超时等。

Configuration

现在,我们已经回顾了该理论,让我们看看如何使用它。要实现 HTTP 摘要验证,必须在过滤器链中定义DigestAuthenticationFilter。应用程序上下文将需要定义DigestAuthenticationFilter及其所需的协作者:

<bean id="digestFilter" class=
    "org.springframework.security.web.authentication.www.DigestAuthenticationFilter">
<property name="userDetailsService" ref="jdbcDaoImpl"/>
<property name="authenticationEntryPoint" ref="digestEntryPoint"/>
<property name="userCache" ref="userCache"/>
</bean>

<bean id="digestEntryPoint" class=
    "org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint">
<property name="realmName" value="Contacts Realm via Digest Authentication"/>
<property name="key" value="acegi"/>
<property name="nonceValiditySeconds" value="10"/>
</bean>

需要配置的UserDetailsService,因为DigestAuthenticationFilter必须可以直接访问用户的明文密码。如果您在 DAO [15]中使用编码密码,则摘要身份验证将不起作用。 DAO 协作者与UserCache通常直接与DaoAuthenticationProvider共享。 authenticationEntryPoint属性必须为DigestAuthenticationEntryPoint,以便DigestAuthenticationFilter可以获取正确的realmNamekey进行摘要计算。

BasicAuthenticationFilter类似,如果身份验证成功,则Authentication请求令牌将被放入SecurityContextHolder。如果身份验证事件成功,或者由于 HTTPHeaders 不包含摘要身份验证请求而未尝试身份验证,则过滤器链将照常 continue。如上一段所述,只有当身份验证失败并调用AuthenticationEntryPoint时,过滤器链才会被中断。

摘要式身份验证的 RFC 提供了一系列附加功能,以进一步提高安全性。例如,可以在每个请求上更改随机数。尽管如此,Spring Security 实现仍被设计为最小化实现的复杂性(以及无疑会出现的用户代理不兼容性),并避免需要存储服务器端状态。如果您希望更详细地探索这些功能,则可以邀请您阅读 RFC 2617.据我们所知,Spring Security 的实现确实符合该 RFC 的最低标准。

10.5 记住我身份验证

10.5.1 Overview

“记住我”或“持久登录”身份验证是指能够记住会话之间的主体身份的网站。通常,这是通过向浏览器发送一个 cookie 来实现的,该 cookie 在以后的会话中被检测到并导致自动登录。 Spring Security 提供了进行这些操作所需的钩子,并具有两个具体的“记住我”实现。一种使用散列来保留基于 cookie 的令牌的安全性,另一种使用数据库或其他持久性存储机制来存储生成的令牌。

请注意,两个实现都需要UserDetailsService。如果您使用的身份验证提供程序不使用UserDetailsService(例如 LDAP 提供程序),那么除非您的应用程序上下文中还包含UserDetailsService bean,否则它将无法工作。

10.5.2 基于哈希的简单令牌方法

这种方法使用哈希来实现有用的“记住我”策略。本质上,在成功进行交互式身份验证后,会将 cookie 发送到浏览器,该 cookie 的组成如下:

base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

username:          As identifiable to the UserDetailsService
password:          That matches the one in the retrieved UserDetails
expirationTime:    The date and time when the remember-me token expires, expressed in milliseconds
key:               A private key to prevent modification of the remember-me token

因此,记住我令牌仅在指定的时间内有效,并且前提是用户名,密码和密钥不变。值得注意的是,这存在潜在的安全问题,因为捕获的“记住我”令牌将可从任何用户代理使用,直到令牌到期为止。这与摘要身份验证相同。如果委托人知道已捕获令牌,则他们可以轻松更改密码并立即使所有出现的“记住我”令牌失效。如果需要更重要的安全性,则应使用下一节所述的方法。另外,根本不应该使用“记住我”服务。

如果您熟悉namespace configuration章节中讨论的主题,则只需添加<remember-me>元素即可启用“记住我”身份验证:

<http>
...
<remember-me key="myAppKey"/>
</http>

UserDetailsService通常会自动选择。如果您的应用程序上下文中有多个,则需要指定user-service-ref属性应使用哪一个,其中值是UserDetailsService bean 的名称。

10.5.3 永久令牌方法

此方法基于文章http://jaspan.com/improved_persistent_login_cookie_best_practice并作了一些较小的修改[16]。要将这种方法与名称空间配置一起使用,您将提供一个数据源参考:

<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>

数据库应包含使用以下 SQL(或等效 SQL)创建的persistent_logins表:

create table persistent_logins (username varchar(64) not null,
                                series varchar(64) primary key,
                                token varchar(64) not null,
                                last_used timestamp not null)

10.5.4 记住我的接口和实现

Remember-me 与UsernamePasswordAuthenticationFilter一起使用,并通过AbstractAuthenticationProcessingFilter超类中的钩子实现。也在BasicAuthenticationFilter内使用。钩子将在适当的时间调用具体的RememberMeServices。界面如下所示:

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response,
    Authentication successfulAuthentication);

请参阅 Javadoc,以更全面地讨论方法的作用,尽管在此阶段请注意AbstractAuthenticationProcessingFilter仅调用loginFail()loginSuccess()方法。每当SecurityContextHolder不包含Authentication时,RememberMeAuthenticationFilter就会调用autoLogin()方法。因此,此接口为底层的“记住我”实现提供了与身份验证相关的事件的充分通知,并在候选 Web 请求可能包含 cookie 并希望被记住时委托给该实现。这种设计允许使用任何数量的“记住我”实施策略。上面我们已经看到,Spring Security 提供了两种实现。我们将依次研究这些。

TokenBasedRememberMeServices

此实现支持第 10.5.2 节“基于哈希的简单令牌方法”中描述的更简单的方法。 TokenBasedRememberMeServices生成RememberMeAuthenticationToken,由RememberMeAuthenticationProvider处理。在此身份验证提供程序和TokenBasedRememberMeServices之间共享key。另外,TokenBasedRememberMeServices需要一个 UserDetailsService,它可以从中检索用户名和密码以进行签名比较,并生成RememberMeAuthenticationToken以包含正确的GrantedAuthority。应用程序应提供某种注销命令,如果用户请求,该命令会使 cookie 无效。 TokenBasedRememberMeServices还实现了 Spring Security 的LogoutHandler接口,因此可以与LogoutFilter一起使用以自动清除 cookie。

在应用程序上下文中启用“记住我”服务所需的 bean 如下:

<bean id="rememberMeFilter" class=
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="theAuthenticationManager" />
</bean>

<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<property name="key" value="springRocks"/>
</bean>

<bean id="rememberMeAuthenticationProvider" class=
"org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="springRocks"/>
</bean>

不要忘记将RememberMeServices实现添加到UsernamePasswordAuthenticationFilter.setRememberMeServices()属性中,将RememberMeAuthenticationProvider包含在AuthenticationManager.setProviders()列表中,并将RememberMeAuthenticationFilter添加到FilterChainProxy中(通常在UsernamePasswordAuthenticationFilter之后)。

PersistentTokenBasedRememberMeServices

可以使用与TokenBasedRememberMeServices相同的方式使用此类,但还需要为其配置PersistentTokenRepository来存储令牌。有两种标准实现。

  • InMemoryTokenRepositoryImpl仅用于测试。

  • JdbcTokenRepositoryImpl将令牌存储在数据库中。

上面的第 10.5.3 节“持久令牌方法”中描述了数据库模式。

10.6 跨站请求伪造(CSRF)

本节讨论 Spring Security 的跨站请求伪造(CSRF)支持。

10.6.1 CSRF 攻击

在讨论 Spring Security 如何保护应用程序免受 CSRF 攻击之前,我们将解释什么是 CSRF 攻击。让我们看一个具体的例子以获得更好的理解。

假设您的银行网站提供了一种表格,该表格允许将资金从当前登录的用户转移到另一个银行帐户。例如,HTTP 请求可能类似于:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

现在,Feign 您对银行的网站进行身份验证,然后在不注销的情况下访问一个邪恶的网站。恶意网站包含具有以下格式的 HTML 页面:

<form action="https://bank.example.com/transfer" method="post">
<input type="hidden"
    name="amount"
    value="100.00"/>
<input type="hidden"
    name="routingNumber"
    value="evilsRoutingNumber"/>
<input type="hidden"
    name="account"
    value="evilsAccountNumber"/>
<input type="submit"
    value="Win Money!"/>
</form>

您想赢钱,因此单击“提交”按钮。在此过程中,您无意中将$ 100 转让给了恶意用户。发生这种情况的原因是,尽管恶意网站无法看到您的 cookie,但与您的银行关联的 cookie 仍与请求一起发送。

最糟糕的是,使用 JavaScript 可以使整个过程自动化。这意味着您甚至不需要单击按钮。那么,我们如何保护自己免受此类攻击呢?

10.6.2 同步器令牌模式

问题在于,来自银行网站的 HTTP 请求与来自邪恶网站的请求完全相同。这意味着无法拒绝来自邪恶网站的请求并允许来自银行网站的请求。为了防御 CSRF 攻击,我们需要确保恶意站点无法提供请求中的某些内容。

一种解决方案是使用同步器令牌模式。该解决方案是确保除我们的会话 cookie 之外,每个请求还需要随机生成的令牌作为 HTTP 参数。提交请求后,服务器必须查找参数的期望值,并将其与请求中的实际值进行比较。如果值不匹配,则请求应失败。

我们可以放宽期望,只要求每个更新状态的 HTTP 请求都需要令牌。可以安全地完成此操作,因为相同的来源策略可确保恶意站点无法读取响应。另外,我们不想在 HTTP GET 中包含随机令牌,因为这可能导致令牌泄漏。

让我们看一下示例将如何变化。假设在名为_csrf 的 HTTP 参数中存在随机生成的令牌。例如,转帐的请求如下所示:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>

您会注意到,我们为_csrf 参数添加了一个随机值。现在邪恶网站将无法猜测_csrf 参数的正确值(必须在邪恶网站上明确提供),并且当服务器将实际令牌与预期令牌进行比较时,传输将失败。

10.6.3 何时使用 CSRF 保护

什么时候应该使用 CSRF 保护?我们的建议是对普通用户可能由浏览器处理的任何请求使用 CSRF 保护。如果仅创建非浏览器 Client 端使用的服务,则可能需要禁用 CSRF 保护。

CSRF 保护和 JSON

一个常见的问题是“我需要保护由 javascript 发出的 JSON 请求吗?”简短的答案是,这取决于。但是,您必须非常小心,因为有些 CSRF 漏洞会影响 JSON 请求。例如,恶意用户可以创建使用以下格式的带有 JSON 的 CSRF

<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
    value="Win Money!"/>
</form>

这将产生以下 JSON 结构

{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

如果应用程序未验证 Content-Type,则该应用程序将被暴露。根据设置的不同,仍然可以通过更新 URL 后缀以“ .json”结尾来利用验证 Content Type 的 Spring MVC 应用程序,如下所示:

<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
    value="Win Money!"/>
</form>

CSRF 和 Stateless 浏览器应用程序

如果我的应用程序是 Stateless 的怎么办?那并不一定意味着您受到保护。实际上,如果用户不需要针对给定请求在 Web 浏览器中执行任何操作,则他们可能仍然容易受到 CSRF 攻击。

例如,考虑一个应用程序使用一个包含其内所有状态的自定义 cookie 而不是 JSESSIONID 进行身份验证。进行 CSRF 攻击后,自定义 cookie 将与请求一起发送,其方式与在前面的示例中发送 JSESSIONID cookie 相同。

使用基本身份验证的用户也容易受到 CSRF 攻击,因为浏览器将在所有请求中自动包括用户名密码,就像在前面的示例中发送 JSESSIONID cookie 一样。

10.6.4 使用 Spring Security CSRF 保护

那么,使用 Spring Security 来保护我们的站点免受 CSRF 攻击的必要步骤是什么?下面概述了使用 Spring Security 的 CSRF 保护的步骤:

使用正确的 HTTP 动词

防御 CSRF 攻击的第一步是确保您的网站使用正确的 HTTP 动词。具体来说,在可以使用 Spring Security 的 CSRF 支持之前,您需要确定您的应用程序正在使用 PATCH,POST,PUT 和/或 DELETE 来修改状态。

这不是 Spring Security 支持的限制,而是对适当的 CSRF 预防的一般要求。原因是在 HTTP GET 中包含私人信息可能导致信息泄漏。有关敏感信息的使用 POST 而不是 GET 的一般指导,请参见RFC 2616 第 15.1.3 节在 URI 中编码敏感信息

配置 CSRF 保护

下一步是在您的应用程序中包括 Spring Security 的 CSRF 保护。一些框架通过使用户会话无效来处理无效的 CSRF 令牌,但这会导致自身的问题。相反,默认情况下,Spring Security 的 CSRF 保护将产生拒绝的 HTTP 403 访问。可以通过将AccessDeniedHandler配置为以不同方式处理InvalidCsrfTokenException进行自定义。

从 Spring Security 4.0 开始,默认情况下使用 XML 配置启用 CSRF 保护。如果要禁用 CSRF 保护,则可以在下面看到相应的 XML 配置。

<http>
    <!-- ... -->
    <csrf disabled="true"/>
</http>

默认情况下,使用 Java 配置会启用 CSRF 保护。如果您想禁用 CSRF,可以在下面看到相应的 Java 配置。有关如何配置 CSRF 保护的更多自定义信息,请参考 csrf()的 Javadoc。

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable();
    }
}

包含 CSRF 令牌

Form Submissions

最后一步是确保在所有 PATCH,POST,PUT 和 DELETE 方法中都包含 CSRF 令牌。一种解决方法是使用_csrf request 属性获取当前的CsrfToken。下面显示了使用 JSP 进行此操作的示例:

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
    method="post">
<input type="submit"
    value="Log out" />
<input type="hidden"
    name="${_csrf.parameterName}"
    value="${_csrf.token}"/>
</form>

一种更简单的方法是使用 Spring Security JSP 标签库中的csrfInput 标签

Note

如果您使用的是 Spring MVC <form:form>标记或Thymeleaf 2.1+并且使用@EnableWebSecurity,则会自动为您提供CsrfToken(使用CsrfRequestDataValueProcessor)。

Ajax 和 JSON 请求

如果您使用的是 JSON,则无法在 HTTP 参数内提交 CSRF 令牌。相反,您可以在 HTTPHeaders 中提交令牌。一种典型的模式是在您的元标记中包含 CSRF 令牌。 JSP 的示例如下所示:

<html>
<head>
    <meta name="_csrf" content="${_csrf.token}"/>
    <!-- default header name is X-CSRF-TOKEN -->
    <meta name="_csrf_header" content="${_csrf.headerName}"/>
    <!-- ... -->
</head>
<!-- ... -->

您可以使用 Spring Security JSP 标签库中更简单的csrfMetaTags tag来代替手动创建 meta 标签。

然后,您可以将令牌包含在所有 Ajax 请求中。如果您使用的是 jQuery,则可以通过以下方式完成:

$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
    xhr.setRequestHeader(header, token);
});
});

作为 jQuery 的替代方法,我们建议使用cujoJS'srest.js。 rest.js模块为以 RESTful 方式处理 HTTP 请求和响应提供了高级支持。核心功能是通过将拦截器链接到 Client 端来根据需要上下文化 HTTPClient 端添加行为的功能。

var client = rest.chain(csrf, {
token: $("meta[name='_csrf']").attr("content"),
name: $("meta[name='_csrf_header']").attr("content")
});

可以与需要向 CSRF 保护的资源发出请求的应用程序的任何组件共享配置的 Client 端。 rest.js 和 jQuery 之间的一个重要区别是,仅使用配置的 Client 端发出的请求将包含 CSRF 令牌,而 jQuery 的* all *请求将包括该令牌。范围限定请求接收令牌的能力有助于防止 CSRF 令牌泄漏给第三方。有关 rest.js 的更多信息,请参考rest.js 参考文档

CookieCsrfTokenRepository

在某些情况下,用户可能希望将CsrfToken保留在 cookie 中。默认情况下,CookieCsrfTokenRepository将写入名为XSRF-TOKEN的 cookie 并从名为X-XSRF-TOKEN的 Headers 或 HTTP 参数_csrf读取它。这些默认值来自AngularJS

您可以使用以下命令以 XML 配置CookieCsrfTokenRepository

<http>
    <!-- ... -->
    <csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
    class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
    p:cookieHttpOnly="false"/>

Note

该示例显式设置cookieHttpOnly=false。这是允许 JavaScript(即 AngularJS)读取它所必需的。如果您不需要直接使用 JavaScript 读取 Cookie 的功能,建议省略cookieHttpOnly=false以提高安全性。

您可以使用以下命令在 Java 配置中配置CookieCsrfTokenRepository

@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

Note

该示例显式设置cookieHttpOnly=false。这是允许 JavaScript(即 AngularJS)读取它所必需的。如果您不需要直接使用 JavaScript 读取 Cookie 的功能,建议省略cookieHttpOnly=false(通过使用new CookieCsrfTokenRepository()来代替)以提高安全性。

10.6.5 CSRF 警告

实施 CSRF 时有一些警告。

Timeouts

一个问题是预期的 CSRF 令牌存储在 HttpSession 中,因此 HttpSession 过期后,您配置的AccessDeniedHandler将收到 InvalidCsrfTokenException。如果您使用默认的AccessDeniedHandler,则浏览器将获得 HTTP 403 并显示错误的错误消息。

Note

有人可能会问,默认情况下,为什么预期的CsrfToken没有存储在 Cookie 中。这是因为存在已知的利用方式,可以由另一个域设置 Headers(即指定 cookie)。这与 Ruby on Rails 当 HeadersX-Requested-With 存在时,不再跳过 CSRF 检查的原因相同。有关如何执行此利用的详细信息,请参见此 webappsec.org 线程。另一个缺点是,通过删除状态(即超时),您将失去在令牌遭到破坏时强制终止令牌的能力。

缓解活动用户超时的一种简单方法是使用一些 JavaScript,该 JavaScript 可以使该用户知道其会话即将到期。用户可以单击一个按钮以 continue 并刷新会话。

另外,指定自定义AccessDeniedHandler可使您以自己喜欢的任何方式处理InvalidCsrfTokenException。有关如何自定义AccessDeniedHandler的示例,请参考提供的xmlJava configuration链接。

最后,可以将应用程序配置为使用CookieCsrfTokenRepository,它将不会过期。如前所述,这并不像使用会话那样安全,但是在许多情况下可以满足要求。

Logging In

为了防御伪造登录请求,登录表单也应受到防御 CSRF 攻击。由于CsrfToken存储在 HttpSession 中,因此这意味着在访问CsrfToken令牌属性后将立即创建 HttpSession。尽管这在 RESTful /Stateless 架构中听起来很糟糕,但现实是状态对于实现实际的安全性是必需的。没有状态,如果令牌被泄露,我们将无能为力。实际上,CSRF 令牌的大小非常小,对我们的架构的影响应该可以忽略不计。

保护表单登录的一种常用技术是在表单提交之前使用 JavaScript 函数获取有效的 CSRF 令牌。这样,无需考虑会话超时(在上一节中讨论过),因为会话是在表单提交之前创建的(假设未配置CookieCsrfTokenRepository),因此用户可以停留在登录页面上并在需要时提交用户名/密码。为了实现这一点,您可以利用 Spring Security 提供的CsrfTokenArgumentResolver并公开一个端点,如here所述。

Logging Out

添加 CSRF 会将 LogoutFilter 更新为仅使用 HTTP POST。这样可以确保注销需要 CSRF 令牌,并且恶意用户不能强制注销用户。

一种方法是使用表单进行注销。如果您确实想要一个链接,则可以使用 JavaScript 来使该链接执行 POST(即可能以隐藏形式)。对于禁用了 JavaScript 的浏览器,您可以选择使该链接将用户带到将执行 POST 的注销确认页面。

如果您确实想在注销时使用 HTTP GET,则可以这样做,但是请记住,通常不建议这样做。例如,以下 Java 配置将使用 URL 执行注销/使用任何 HTTP 方法请求注销:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
    }
}

分段(文件上传)

将 CSRF 保护与 multipart/form-data 一起使用有两种选择。每个选项都有其权衡。

Note

在将 Spring Security 的 CSRF 保护与分段文件上传集成之前,请确保您可以首先在没有 CSRF 保护的情况下进行上传。有关在 Spring 上使用 Multipart 表单的更多信息,请参见 Spring 参考的17.10 Spring 的 Multipart(文件上传)支持部分和MultipartFilter javadoc

在 Spring Security 之前放置 MultipartFilter

第一种选择是确保在 Spring Security 过滤器之前指定MultipartFilter。在 Spring Security 过滤器之前指定MultipartFilter意味着没有授权调用MultipartFilter,这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。

为了确保在使用 Java 配置的 Spring Security 过滤器之前指定了MultipartFilter,用户可以如下所示覆盖 beforeSpringSecurityFilterChain:

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        insertFilters(servletContext, new MultipartFilter());
    }
}

为了确保在具有 XML 配置的 Spring Security 过滤器之前指定MultipartFilter,用户可以确保MultipartFilter的\ 元素位于 web.xml 中的 springSecurityFilterChain 之前,如下所示:

<filter>
    <filter-name>MultipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>MultipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
实际使用 CSRF 令牌

如果不允许未经授权的用户上传临时文件,则可以选择将MultipartFilter放在 Spring Security 过滤器之后,并将 CSRF 作为查询参数包括在表单的 action 属性中。带有 jsp 的示例如下所示

<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">

这种方法的缺点是查询参数可能会泄漏。一般而言,将敏感数据放置在正文或标题中以确保其不被泄露是最佳实践。其他信息可以在RFC 2616 第 15.1.3 节在 URI 中编码敏感信息中找到。

HiddenHttpMethodFilter

HiddenHttpMethodFilter 应该放在 Spring Security 过滤器之前。总的来说,这是正确的,但在防御 CSRF 攻击时可能会有其他含义。

请注意,HiddenHttpMethodFilter 仅覆盖 POST 上的 HTTP 方法,因此实际上不太可能引起任何实际问题。但是,仍然最好的方法是确保将其放置在 Spring Security 的过滤器之前。

10.6.6 覆盖默认值

Spring Security 的目标是提供默认值,以保护您的用户免遭攻击。这并不意味着您被迫接受其所有默认值。

例如,您可以提供一个自定义 CsrfTokenRepository 来覆盖CsrfToken的存储方式。

您还可以指定一个自定义的 RequestMatcher 来确定哪些请求受 CSRF 保护(即,您可能不在乎是否利用了注销)。简而言之,如果 Spring Security 的 CSRF 保护的行为不完全符合您的期望,则可以自定义行为。有关使用 XML 进行这些自定义的详细信息,请参见名为“<csrf>”的部分文档,有关使用 Java 配置时如何进行这些自定义的详细信息,请参考CsrfConfigurer javadoc。

10.7 CORS

Spring Framework 提供了对 CORS 的一流支持。必须在 Spring Security 之前处理 CORS,因为飞行前请求将不包含任何 cookie(即JSESSIONID)。如果请求不包含任何 cookie,并且首先使用 Spring Security,则该请求将确定用户未通过身份验证(因为请求中没有 cookie),并拒绝该用户。

确保首先处理 CORS 的最简单方法是使用CorsFilter。用户可以使用以下代码提供CorsConfigurationSource来将CorsFilter与 Spring Security 集成:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // by default uses a Bean by the name of corsConfigurationSource
            .cors().and()
            ...
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

或 XML

<http>
    <cors configuration-source-ref="corsSource"/>
    ...
</http>
<b:bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
    ...
</b:bean>

如果您使用的是 Spring MVC 的 CORS 支持,则可以省略指定CorsConfigurationSource,并且 Spring Security 将利用提供给 Spring MVC 的 CORS 配置。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // if Spring MVC is on classpath and no CorsConfigurationSource is provided,
            // Spring Security will use CORS configuration provided to Spring MVC
            .cors().and()
            ...
    }
}

或 XML

<http>
    <!-- Default to Spring MVC's CORS configuration -->
    <cors />
    ...
</http>

10.8 安全 HTTP 响应 Headers

本节讨论了 Spring Security 对向响应添加各种安全 Headers 的支持。

10.8.1 默认安全标题

Spring Security 允许用户轻松注入默认的安全 Headers,以帮助保护其应用程序。 Spring Security 的默认值为包含以下 Headers:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Note

仅在 HTTPS 请求上添加严格传输安全性

有关这些标题中的每个标题的更多详细信息,请参阅相应的部分:

虽然这些 Headers 中的每一个均被视为最佳实践,但应注意,并非所有 Client 端都使用 Headers,因此鼓励进行其他测试。

您可以自定义特定的标题。例如,假设希望您的 HTTP 响应 Headers 如下所示:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block

具体来说,您希望所有默认 Headers 都具有以下自定义设置:

您可以使用以下 Java 配置轻松完成此操作:

@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers()
                .frameOptions().sameOrigin()
                .httpStrictTransportSecurity().disable();
    }
}

另外,如果您使用的是 Spring Security XML Configuration,则可以使用以下代码:

<http>
    <!-- ... -->

    <headers>
        <frame-options policy="SAMEORIGIN" />
        <hsts disable="true"/>
    </headers>
</http>

如果您不想添加默认值,并且希望对应使用的内容进行明确控制,则可以禁用默认值。下面提供了基于 Java 和 XML 的配置示例:

如果您使用的是 Spring Security 的 Java 配置,则以下内容只会添加Cache Control

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        // do not use any default headers unless explicitly listed
        .defaultsDisabled()
        .cacheControl();
}
}

以下 XML 仅会添加Cache Control

<http>
    <!-- ... -->

    <headers defaults-disabled="true">
        <cache-control/>
    </headers>
</http>

如有必要,可以使用以下 Java 配置禁用所有 HTTP 安全响应 Headers:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers().disable();
}
}

如有必要,您可以使用以下 XML 配置禁用所有 HTTP 安全响应 Headers:

<http>
    <!-- ... -->

    <headers disabled="true" />
</http>

Cache Control

过去,Spring Security 要求您为 Web 应用程序提供自己的缓存控件。当时看来这是合理的,但浏览器缓存已 Developing 为包括用于安全连接的缓存。这意味着用户可以查看经过身份验证的页面,然后注销,然后恶意用户可以使用浏览器历史记录来查看缓存的页面。为了帮助缓解这种情况,Spring Security 添加了缓存控制支持,该支持将在响应中插入以下 Headers。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0

只需添加没有子元素的<headers>元素即可自动添加 Cache Control 和许多其他保护。但是,如果仅需要缓存控制,则可以使用 Spring Security 的 XML 名称空间(带有<cache-control>元素和[email protected]属性)来启用此功能。

<http>
    <!-- ... -->

    <headers defaults-disable="true">
        <cache-control />
    </headers>
</http>

同样,您可以使用以下命令在 Java 配置中仅启用缓存控制:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .defaultsDisabled()
        .cacheControl();
}
}

如果您确实想缓存特定的响应,则您的应用程序可以有选择地调用HttpServletResponse.setHeader(String,String)来覆盖 Spring Security 设置的 Headers。这对于确保正确缓存 CSS,JavaScript 和图像之类的内容很有用。

使用 Spring Web MVC 时,通常在您的配置中完成。例如,以下配置将确保为您的所有资源设置缓存头:

@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
            .addResourceHandler("/resources/**")
            .addResourceLocations("/resources/")
            .setCachePeriod(31556926);
    }

    // ...
}

Content Type 选项

历史上,包括 Internet Explorer 在内的浏览器都会尝试使用content sniffing来猜测请求的 Content Type。这允许浏览器通过猜测未指定 Content Type 的资源上的 Content Type 来改善用户体验。例如,如果浏览器遇到一个未指定 Content Type 的 JavaScript 文件,它将能够猜测该 Content Type 然后执行它。

Note

允许上传内容时,还有许多其他事情(即,仅在不同的域中显示文档,确保设置了 Content-TypeHeaders,清理文档等)。但是,这些措施不在 Spring Security 提供的范围之内。同样重要的是要指出在禁用内容嗅探时,必须指定 Content Type 才能使内容正常工作。

内容嗅探的问题在于,这允许恶意用户使用多义标记(即,可以作为多种 Content Type 有效的文件)执行 XSS 攻击。例如,某些网站可能允许用户向网站提交有效的附言文档并进行查看。恶意用户可能会创建后记文档也是有效的 JavaScript 文件并对其执行 XSS 攻击。

可以通过在响应中添加以下 Headers 来禁用内容嗅探:

X-Content-Type-Options: nosniff

与高速缓存控制元素一样,在使用不带子元素的\ 元素时,默认情况下会添加 nosniff 指令。但是,如果您想进一步控制要添加的 Headers,可以使用<content-type-options>元素和[email protected]属性,如下所示:

<http>
    <!-- ... -->

    <headers defaults-disabled="true">
        <content-type-options />
    </headers>
</http>

默认情况下,Spring Security Java 配置添加了 X-Content-Type-Options 头。如果要对标题进行更多控制,则可以使用以下命令显式指定 Content Type 选项:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .defaultsDisabled()
        .contentTypeOptions();
}
}

HTTP 严格传输安全性(HSTS)

当您 Importing 银行的网站时,您 Importingmybank.example.com 还是 Importinghttps://mybank.example.com?如果您忽略 https 协议,则可能会受到中间人袭击的攻击。即使网站执行到https://mybank.example.com的重定向,恶意用户也可能拦截初始 HTTP 请求并操纵响应(即,重定向到https://mibank.example.com并窃取其凭据)。

许多用户忽略了 https 协议,这就是创建HTTP 严格传输安全性(HSTS)的原因。将 mybank.example.com 添加为HSTS host后,浏览器可以提前知道对 mybank.example.com 的任何请求都应解释为https://mybank.example.com。这大大降低了发生中间人攻击的可能性。

Note

根据RFC6797,HSTSHeaders 仅注入 HTTPS 响应中。为了使浏览器能够确认 Headers,浏览器必须首先信任对用于构建连接的 SSL 证书(不仅仅是 SSL 证书)进行签名的 CA。

将站点标记为 HSTS 主机的一种方法是将主机预加载到浏览器中。另一个方法是在响应中添加“ Strict-Transport-Security”Headers。例如,以下内容将指示浏览器将域视为一年的 HSTS 主机(一年大约 31536000 秒):

Strict-Transport-Security: max-age=31536000 ; includeSubDomains

可选的 includeSubDomains 指令指示 Spring Security 子域(即 secure.mybank.example.com)也应被视为 HSTS 域。

与其他头文件一样,Spring Security 默认添加 HSTS。您可以使用<hsts>元素来自定义 HSTSHeaders,如下所示:

<http>
    <!-- ... -->

    <headers>
        <hsts
            include-subdomains="true"
            max-age-seconds="31536000" />
    </headers>
</http>

同样,您只能使用 Java 配置启用 HSTSHeaders:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .httpStrictTransportSecurity()
            .includeSubdomains(true)
            .maxAgeSeconds(31536000);
}
}

HTTP 公钥固定(HPKP)

HTTP 公共密钥固定(HPKP)是一项安全功能,它告诉 WebClient 端将特定的加密公共密钥与特定的 Web 服务器相关联,以防止伪造证书对中间人(MITM)的攻击。

为了确保 TLS 会话中使用的服务器公钥的真实性,此公钥被包装到 X.509 证书中,该证书通常由证书颁发机构(CA)签名。浏览器之类的 WebClient 端信任许多这样的 CA,它们都可以为任意域名创建证书。如果攻击者能够入侵单个 CA,则他们可以对各种 TLS 连接执行 MITM 攻击。 HPKP 可以通过告诉 Client 端哪个公钥属于某个 Web 服务器来规避 HTTPS 协议的这种威胁。 HPKP 是首次使用信任(TOFU)技术。 Web 服务器第一次通过特殊的 HTTPHeaders 告诉 Client 端哪个公钥属于它时,Client 端会在给定的时间段内存储此信息。当 Client 端再次访问服务器时,它期望包含公钥的证书,该公钥的指纹已经通过 HPKP 知道。如果服务器提供了未知的公共密钥,则 Client 端应向用户显示警告。

Note

因为用户代理需要根据 SSL 证书链验证引脚,所以 HPKPHeaders 仅注入 HTTPS 响应中。

为您的站点启用此功能就像通过 HTTPS 访问站点时返回 Public-Key-Pins HTTPHeaders 一样简单。例如,以下内容将指示用户代理仅将 2 个引脚的引脚验证失败报告给给定 URI(通过report-uri指令):

Public-Key-Pins-Report-Only: max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=" ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="http://example.net/pkp-report" ; includeSubDomains

引脚验证失败报告是标准 JSON 结构,可以通过 Web 应用程序自己的 API 或公共托管的 HPKP 报告服务(例如REPORT-URI)捕获。

可选的 includeSubDomains 指令指示浏览器也使用给定的引脚来验证子域。

与其他 Headers 相反,Spring Security 默认情况下不添加 HPKP。您可以使用<hpkp>元素来自定义 HPKPHeaders,如下所示:

<http>
    <!-- ... -->

    <headers>
        <hpkp
            include-subdomains="true"
            report-uri="http://example.net/pkp-report">
            <pins>
                    <pin algorithm="sha256">d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=</pin>
                    <pin algorithm="sha256">E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=</pin>
            </pins>
        </hpkp>
    </headers>
</http>

同样,您可以使用 Java 配置启用 HPKPHeaders:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
                http
                // ...
                .headers()
                        .httpPublicKeyPinning()
                                .includeSubdomains(true)
                                .reportUri("http://example.net/pkp-report")
                                .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
        }
}

X-Frame-Options

允许将您的网站添加到框架可能是一个安全问题。例如,使用聪明的 CSS 样式用户可能会被诱骗点击他们不想要的内容(video demo)。例如,登录到其银行的用户可以单击将按钮授予其他用户访问权限。这种攻击称为Clickjacking

Note

处理点击劫持的另一种现代方法是使用称为“内容安全策略(CSP)”的部分

有许多方法可以缓解点击劫持攻击。例如,要保护旧版浏览器免受点击劫持攻击,可以使用帧破码。虽然不完美,但是对于传统浏览器而言,破帧代码是最好的选择。

解决点击劫持的更现代方法是使用X-Frame-OptionsHeaders:

X-Frame-Options: DENY

X-Frame-Options 响应 Headers 指示浏览器阻止响应中带有此 Headers 的任何网站呈现在框架中。默认情况下,Spring Security 禁用 iframe 中的呈现。

您可以使用frame-options元素自定义 X-Frame-Options。例如,以下内容将指示 Spring Security 使用“ X-Frame-Options:SAMEORIGIN”,它允许同一域内的 iframe:

<http>
    <!-- ... -->

    <headers>
        <frame-options
        policy="SAMEORIGIN" />
    </headers>
</http>

同样,您可以使用以下方法自定义框架选项以在 Java 配置中使用相同的来源:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .frameOptions()
            .sameOrigin();
}
}

X-XSS-Protection

一些浏览器内置了对过滤反映的 XSS 攻击的支持。这绝非万无一失,但确实有助于 XSS 保护。

通常默认情况下会启用过滤,因此添加 Headers 通常只会确保 Headers 已启用,并指示浏览器在检测到 XSS 攻击时应采取的措施。例如,过滤器可能会尝试以最小侵入性的方式更改内容以仍然呈现所有内容。有时,这种替换可以成为XSS 漏洞本身。相反,最好是阻止内容,而不要尝试对其进行修复。为此,我们可以添加以下 Headers:

X-XSS-Protection: 1; mode=block

默认情况下包含此 Headers。但是,我们可以根据需要自定义它。例如:

<http>
    <!-- ... -->

    <headers>
        <xss-protection block="false"/>
    </headers>
</http>

同样,您可以使用以下命令在 Java 配置中自定义 XSS 保护:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .xssProtection()
            .block(false);
}
}

内容安全 Policy(CSP)

内容安全 Policy(CSP)是 Web 应用程序可以利用的一种机制来减轻内容注入漏洞,例如跨站点脚本(XSS)。 CSP 是一种声明性策略,为 Web 应用程序作者提供了一种工具,可以声明该 Web 应用程序希望从中加载资源的来源,并最终将这些信息通知 Client 端(用户代理)。

Note

内容安全策略并非旨在解决所有内容注入漏洞。取而代之的是,可以利用 CSP 帮助减少内容注入攻击所造成的危害。作为第一道防线,Web 应用程序作者应验证其 Importing 并对输出进行编码。

Web 应用程序可以通过在响应中包括以下 HTTPHeaders 之一来使用 CSP:

  • Content-Security-Policy

  • Content-Security-Policy-Report-Only

这些 Headers 中的每一个都用作向 Client 端传递 *安全策略 *的机制。安全策略包含一组 *安全策略指令 (例如 script-src object-src *),每个指令负责声明对特定资源表示形式的限制。

例如,Web 应用程序可以通过在响应中包括以下 Headers 来声明它希望从特定的受信任源中加载脚本:

Content-Security-Policy: script-src https://trustedscripts.example.com

尝试从* script-src *指令中未声明的其他来源加载脚本的尝试将被用户代理阻止。此外,如果在安全策略中声明了report-uri指令,则用户代理会将违规报告到声明的 URL。

例如,如果 Web 应用程序违反了声明的安全策略,则以下响应 Headers 将指示用户代理将违规报告发送到策略的* report-uri *指令中指定的 URL。

Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/

Violation reports是标准 JSON 结构,可以由 Web 应用程序自己的 API 或由公共托管的 CSP 违规报告服务(例如REPORT-URI)捕获。

*** Content-Security-Policy-Report-Only** Headers 为 Web 应用程序作者和 Management 员提供了监视安全策略而不是强制执行这些策略的功能。该标题通常在试验和/或开发站点的安全策略时使用。当某个策略被认为有效时,可以通过使用 Content-Security-Policy *Headers 字段来实施该策略。

给定以下响应头,该策略声明可以从两个可能的来源之一加载脚本。

Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/

如果网站违反了此 Policy,则通过尝试从* evil.com 加载脚本,用户代理会将违规报告发送到 report-uri *指令指定的声明的 URL,但仍允许违规资源尽管如此。

配置内容安全策略

重要的是要注意,Spring Security *默认不添加 * Content Security Policy。 Web 应用程序作者必须声明安全策略以强制执行和/或监视受保护的资源。

例如,给定以下安全策略:

script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/

您可以使用带有<content-security-policy>元素的 XML 配置来启用 CSPHeaders,如下所示:

<http>
    <!-- ... -->

    <headers>
        <content-security-policy
            policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" />
    </headers>
</http>

要启用 CSP *'report-only'*Headers,请按以下方式配置元素:

<http>
    <!-- ... -->

    <headers>
        <content-security-policy
            policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"
            report-only="true" />
    </headers>
</http>

同样,您可以使用 Java 配置启用 CSPHeaders,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/");
}
}

要启用 CSP *'report-only'*Headers,请提供以下 Java 配置:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
        .reportOnly();
}
}
Additional Resources

将内容安全策略应用于 Web 应用程序通常是一项艰巨的任务。以下资源可以为您的站点制定有效的安全策略提供进一步的帮助。

内容安全策略简介

CSP 指南-Mozilla 开发人员网络

W3C 候选人推荐

Referrer Policy

Referrer Policy是一种机制,Web 应用程序可以利用该机制来 Management 引荐来源网址字段,该字段包含用户所在的最后一页。

Spring Security 的方法是使用Referrer PolicyHeaders,它提供了不同的policies

Referrer-Policy: same-origin

Referrer-Policy 响应 Headers 指示浏览器让目的地知道用户先前所在的源。

配置引荐来源网址策略

Spring Security *不会添加 * Referrer PolicyHeaders 默认情况下。

您可以使用带有<referrer-policy>元素的 XML 配置来启用 Referrer-PolicyHeaders,如下所示:

<http>
    <!-- ... -->

    <headers>
        <referrer-policy policy="same-origin" />
    </headers>
</http>

同样,您可以使用 Java 配置启用 Referrer PolicyHeaders,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .referrerPolicy(ReferrerPolicy.SAME_ORIGIN);
}
}

Feature Policy

Feature Policy是一种机制,允许 Web 开发人员在浏览器中选择性地启用,禁用和修改某些 API 和 Web 功能的行为。

Feature-Policy: geolocation 'self'

借助功能策略,开发人员可以为浏览器选择一套“策略”,以实施整个站点中使用的特定功能。这些策略限制了网站可以访问或修改某些功能的浏览器默认行为的 API。

配置功能部件策略

Spring Security *默认不添加 * Feature PolicyHeaders。

您可以使用带有<feature-policy>元素的 XML 配置来启用 Feature-PolicyHeaders,如下所示:

<http>
    <!-- ... -->

    <headers>
        <feature-policy policy-directives="geolocation 'self'" />
    </headers>
</http>

同样,您可以使用 Java 配置启用功能策略 Headers,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .featurePolicy("geolocation 'self'");
}
}

10.8.2 自定义标题

Spring Security 具有使您可以方便地将更常见的安全 Headers 添加到您的应用程序的机制。但是,它也提供了钩子来启用添加自定义 Headers。

Static Headers

有时您可能希望将不支持的自定义安全 Headers 注入应用程序中。例如,给定以下自定义安全 Headers:

X-Custom-Security-Header: header-value

使用 XML 名称空间时,可以使用<header>元素将这些 Headers 添加到响应中,如下所示:

<http>
    <!-- ... -->

    <headers>
        <header name="X-Custom-Security-Header" value="header-value"/>
    </headers>
</http>

同样,可以使用 Java 配置将 Headers 添加到响应中,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value"));
}
}

Headers Writer

如果名称空间或 Java 配置不支持所需的 Headers,则可以创建自定义HeadersWriter实例,甚至提供HeadersWriter的自定义实现。

我们来看一个使用XFrameOptionsHeaderWriter的自定义实例的示例。也许您希望允许对同一来源的内容进行框架化。通过将policy属性设置为“ SAMEORIGIN”可以轻松地支持此功能,但是让我们来看一个使用ref属性的更明确的示例。

<http>
    <!-- ... -->

    <headers>
        <header ref="frameOptionsWriter"/>
    </headers>
</http>
<!-- Requires the c-namespace.
See http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-c-namespace
-->
<beans:bean id="frameOptionsWriter"
    class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"
    c:frameOptionsMode="SAMEORIGIN"/>

我们还可以使用 Java 配置将内容框架限制为相同来源:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    // ...
    .headers()
        .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN));
}
}

DelegatingRequestMatcherHeaderWriter

有时您可能只想为某些请求编写 Headers。例如,也许您只想保护登录页面免于陷害。您可以使用DelegatingRequestMatcherHeaderWriter来这样做。使用 XML 名称空间配置时,可以使用以下方法完成:

<http>
    <!-- ... -->

    <headers>
        <frame-options disabled="true"/>
        <header ref="headerWriter"/>
    </headers>
</http>

<beans:bean id="headerWriter"
    class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter">
    <beans:constructor-arg>
        <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher"
            c:pattern="/login"/>
    </beans:constructor-arg>
    <beans:constructor-arg>
        <beans:bean
            class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"/>
    </beans:constructor-arg>
</beans:bean>

我们还可以使用 Java 配置防止将内容 Framework 到登录页面:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    RequestMatcher matcher = new AntPathRequestMatcher("/login");
    DelegatingRequestMatcherHeaderWriter headerWriter =
        new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter());
    http
    // ...
    .headers()
        .frameOptions().disabled()
        .addHeaderWriter(headerWriter);
}
}

10.9 会话 Management

HTTP 会话相关的功能由过滤器委托的SessionManagementFilterSessionAuthenticationStrategy接口组合处理。典型的用法包括防止会话固定保护攻击,检测会话超时以及限制已认证用户可以同时打开多少个会话。

10.9.1 SessionManagementFilter

SessionManagementFilter对照SecurityContextHolder的当前内容检查SecurityContextRepository的内容,以确定用户是否在当前请求期间已通过典型的非交互式身份验证机制(例如,预身份验证或“记住我的名字” [17])进行了身份验证。如果存储库包含安全上下文,则过滤器不执行任何操作。如果不是,并且线程本地SecurityContext包含(非匿名)Authentication对象,则过滤器将假定它们已由堆栈中的先前过滤器进行了身份验证。然后它将调用已配置的SessionAuthenticationStrategy

如果用户当前未通过身份验证,则过滤器将检查是否已请求了无效的会话 ID(例如,由于超时),并且将调用配置的InvalidSessionStrategy(如果已设置)。最常见的行为只是重定向到固定 URL,并将其封装在标准实现SimpleRedirectInvalidSessionStrategy中。通过名称空间如前所述配置无效的会话 URL 时,也会使用后者。

10.9.2 SessionAuthenticationStrategy

SessionManagementFilterAbstractAuthenticationProcessingFilter都使用SessionAuthenticationStrategy,因此,例如,如果您使用自定义的表单登录类,则需要将其注入这两个类中。在这种情况下,将名称空间和定制 Bean 相结合的典型配置如下所示:

<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <beans:property name="sessionAuthenticationStrategy" ref="sas" />
    ...
</beans:bean>

<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />

请注意,如果您在实现HttpSessionBindingListener的会话中存储 Bean(包括 Spring 会话范围的 Bean),则使用默认值SessionFixationProtectionStrategy可能会引起问题。有关更多信息,请参见 Javadoc。

10.9.3 并发控制

Spring Security 可以防止主体同时向同一应用程序进行身份验证超过指定次数。许多 ISV 都利用此功能来实施许可,而网络 Management 员喜欢此功能,因为它有助于防止人们共享登录名。例如,您可以阻止用户“蝙蝠侠”从两个不同的会话登录到 Web 应用程序。您可以使他们的前一次登录到期,也可以在他们尝试再次登录时报告错误,从而阻止第二次登录。请注意,如果您使用第二种方法,则尚未明确注销的用户(例如,刚刚关闭浏览器的用户)将无法再次登录,直到他们的原始会话期满为止。

名称空间支持并发控制,因此,请查阅前面的名称空间一章以获取最简单的配置。有时您需要自定义内容。

该实现使用名为ConcurrentSessionControlAuthenticationStrategy的专用版本SessionAuthenticationStrategy

Note

以前,并发身份验证检查是由ProviderManager进行的,可以通过ConcurrentSessionController进行注入。后者将检查用户是否试图超过允许的会话数。但是,这种方法要求预先创建 HTTP 会话,这是不希望的。在 Spring Security 3 中,用户首先通过AuthenticationManager进行身份验证,一旦成功通过身份验证,就会创建一个会话,并检查是否允许他们打开另一个会话。

要使用并发会话支持,您需要将以下内容添加到web.xml

<listener>
    <listener-class>
    org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
</listener>

另外,您需要将ConcurrentSessionFilter添加到FilterChainProxyConcurrentSessionFilter需要两个构造函数参数sessionRegistry(通常指向SessionRegistryImpl的实例)和sessionInformationExpiredStrategy(定义会话过期时要应用的策略)。使用名称空间创建FilterChainProxy和其他默认 bean 的配置可能如下所示:

<http>
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />

<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="redirectSessionInformationExpiredStrategy"
class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy">
<beans:constructor-arg name="invalidSessionUrl" value="/session-expired.htm" />
</beans:bean>

<beans:bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:constructor-arg name="sessionInformationExpiredStrategy" ref="redirectSessionInformationExpiredStrategy" />
</beans:bean>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>

<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg>
    <beans:list>
    <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
        <beans:constructor-arg ref="sessionRegistry"/>
        <beans:property name="maximumSessions" value="1" />
        <beans:property name="exceptionIfMaximumExceeded" value="true" />
    </beans:bean>
    <beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
    </beans:bean>
    <beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
        <beans:constructor-arg ref="sessionRegistry"/>
    </beans:bean>
    </beans:list>
</beans:constructor-arg>
</beans:bean>

<beans:bean id="sessionRegistry"
    class="org.springframework.security.core.session.SessionRegistryImpl" />

将监听器添加到web.xml时,每次HttpSession开始或终止时,ApplicationEvent都会发布到 Spring ApplicationContext。这很关键,因为它允许会话结束时通知SessionRegistryImpl。没有它,用户将无法再次登录,一旦他们超出了会话限制,即使他们退出了另一个会话或超时。

查询 SessionRegistry 中当前经过身份验证的用户及其会话

通过名称空间或使用普通 bean 设置并发控制具有有益的副作用,即为您提供了对SessionRegistry的引用,您可以在应用程序中直接使用该SessionRegistry,因此即使您不想限制用户可能拥有的会话,无论如何都值得构建基础架构。您可以将maximumSession属性设置为-1 以允许无限制的会话。如果使用的是命名空间,则可以使用session-registry-alias属性为内部创建的SessionRegistry设置别名,并提供一个可以注入到您自己的 bean 中的引用。

getAllPrincipals()方法向您提供当前已认证用户的列表。您可以通过调用getAllSessions(Object principal, boolean includeExpiredSessions)方法列出用户的会话,该方法返回SessionInformation对象的列表。您还可以通过在SessionInformation实例上调用expireNow()来终止用户的会话。当用户返回到应用程序时,将阻止他们 continue 操作。例如,您可能会发现这些方法在 Management 应用程序中很有用。看一下 Javadoc 以获得更多信息。

10.10 匿名身份验证

10.10.1 Overview

通常,采用“默认情况下拒绝”的做法被认为是一种良好的安全做法,您可以在其中明确指定允许的内容并禁止其他所有内容。定义未经身份验证的用户可以访问的内容的情况与此类似,尤其是对于 Web 应用程序。许多站点要求用户必须通过身份验证才能使用少数几个 URL(例如,主页和登录页面)。在这种情况下,最简单的是为这些特定的 URL 定义访问配置属性,而不是为每个受保护的资源定义访问配置属性。换句话说,有时最好说ROLE_SOMETHING是默认值,并且只允许该规则的某些 exception,例如登录,注销和应用程序的主页。您也可以从过滤器链中完全省略这些页面,从而绕过访问控制检查,但是由于其他原因,这可能是不希望的,尤其是对于经过身份验证的用户而言,这些页面的行为不同时。

这就是我们所说的匿名身份验证。请注意,“匿名身份验证”的用户和未经身份验证的用户之间没有 true 的概念差异。 Spring Security 的匿名身份验证只是为您提供了一种更便捷的方式来配置访问控制属性。例如,即使SecurityContextHolder中实际上存在一个匿名身份验证对象,对 Servlet API 调用的调用(例如getCallerPrincipal)仍将返回 null。

在其他情况下,匿名身份验证很有用,例如当审核拦截器查询SecurityContextHolder以确定哪个主体负责给定操作时。如果类知道SecurityContextHolder始终包含Authentication对象,而从不包含null,则可以更强大地编写类。

10.10.2 Configuration

使用 HTTP 配置 Spring Security 3.0 时会自动提供匿名身份验证支持,并且可以使用<anonymous>元素进行自定义(或禁用)匿名身份验证。除非您使用传统的 Bean 配置,否则无需配置此处描述的 Bean。

三个类共同提供了匿名身份验证功能。 AnonymousAuthenticationTokenAuthentication的实现,并存储适用于匿名主体的GrantedAuthority。有一个对应的AnonymousAuthenticationProvider,它链接到ProviderManager,因此AnonymousAuthenticationToken被接受。最后,有一个AnonymousAuthenticationFilter,它在常规身份验证机制之后被链接,如果那里没有现有的Authentication,则会自动将AnonymousAuthenticationToken添加到SecurityContextHolder。筛选器和身份验证提供程序的定义如下所示:

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

<bean id="anonymousAuthenticationProvider"
    class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>

过滤器和身份验证提供程序之间共享key,因此前者[18]接受前者创建的令牌。 userAttributeusernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]的形式表示。这与InMemoryDaoImpluserMap属性的等号之后使用的语法相同。

如前所述,匿名身份验证的好处是所有 URI 模式都可以应用安全性。例如:

<bean id="filterSecurityInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="securityMetadata">
    <security:filter-security-metadata-source>
    <security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/**' access='ROLE_USER'/>
    </security:filter-security-metadata-source>" +
</property>
</bean>

10.10.3 AuthenticationTrustResolver

AuthenticationTrustResolver接口及其相应的AuthenticationTrustResolverImpl实现完善了匿名身份验证讨论。此接口提供isAnonymous(Authentication)方法,该方法允许感兴趣的类将这种特殊类型的身份验证状态考虑在内。 ExceptionTranslationFilter在处理AccessDeniedException时使用此接口。如果抛出AccessDeniedException,并且身份验证是匿名类型,则代替引发 403(禁止)响应,过滤器将开始AuthenticationEntryPoint,以便委托人可以正确地进行身份验证。这是必要的区别,否则主体将始终被视为“已认证”,并且永远不会获得通过表单,基本,摘要或某些其他常规认证机制进行登录的机会。

您通常会在上述拦截器配置中看到ROLE_ANONYMOUS属性被IS_AUTHENTICATED_ANONYMOUSLY替换,这在定义访问控制时实际上是相同的。这是AuthenticatedVoter用法的示例,我们将在authorization chapter中看到。它使用AuthenticationTrustResolver处理此特定的配置属性,并将访问权限授予匿名用户。 AuthenticatedVoter方法更强大,因为它使您能够区分匿名用户,记住我的用户和经过完全认证的用户。如果您不需要此功能,则可以坚持使用ROLE_ANONYMOUS,它将由 Spring Security 的标准RoleVoter处理。

10.11 WebSocket 安全

Spring Security 4 增加了对保护Spring 的 WebSocket 支持的支持。本节描述了如何使用 Spring Security 的 WebSocket 支持。

Note

您可以在https://github.com/spring-projects/spring-session/tree/master/samples/boot/websocket找到完整的 WebSocket 安全性工作 samples。

Direct JSR-356 Support

Spring Security 不提供直接的 JSR-356 支持,因为这样做几乎没有价值。这是因为格式未知,所以有Spring 可以做些什么来保护未知格式。另外,JSR-356 没有提供拦截消息的方法,因此安全性将具有相当大的侵入性。

10.11.1 WebSocket 配置

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSocket 的授权支持。要使用 Java 配置来配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。例如:

@Configuration
public class WebSocketSecurityConfig
      extends AbstractSecurityWebSocketMessageBrokerConfigurer { (1) (2)

    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .simpDestMatchers("/user/*").authenticated() (3)
    }
}

这将确保:

  • (1) 任何入站 CONNECT 消息都需要有效的 CSRF 令牌才能实施同源 Policy
  • (2) 对于任何入站请求,在 simpUserHeaders 属性内用该用户填充 SecurityContextHolder。
  • (3) 我们的消息需要适当的授权。具体来说,任何以“/user /”开头的入站消息都需要 ROLE_USER。有关授权的其他详细信息,请参见第 10.11.3 节“ WebSocket 授权”

Spring Security 还提供XML Namespace支持以保护 WebSocket。可比较的基于 XML 的配置如下所示:

<websocket-message-broker> (1) (2)
    (3)
    <intercept-message pattern="/user/**" access="hasRole('USER')" />
</websocket-message-broker>

这将确保:

  • (1) 任何入站 CONNECT 消息都需要有效的 CSRF 令牌才能实施同源 Policy
  • (2) 对于任何入站请求,在 simpUserHeaders 属性内用该用户填充 SecurityContextHolder。
  • (3) 我们的消息需要适当的授权。具体来说,任何以“/user /”开头的入站消息都需要 ROLE_USER。有关授权的其他详细信息,请参见第 10.11.3 节“ WebSocket 授权”

10.11.2 WebSocket 身份验证

WebSockets 重用构建 WebSocket 连接时在 HTTP 请求中找到的相同身份验证信息。这意味着HttpServletRequest上的Principal将移交给 WebSockets。如果您使用的是 Spring Security,则HttpServletRequest上的Principal将被自动覆盖。

更具体地说,要确保用户已通过 WebSocket 应用程序的身份验证,所需要做的就是确保您将 Spring Security 设置为对基于 HTTP 的 Web 应用程序进行身份验证。

10.11.3 WebSocket 授权

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSocket 的授权支持。要使用 Java 配置来配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。例如:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .nullDestMatcher().authenticated() (1)
                .simpSubscribeDestMatchers("/user/queue/errors").permitAll() (2)
                .simpDestMatchers("/app/**").hasRole("USER") (3)
                .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") (4)
                .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() (5)
                .anyMessage().denyAll(); (6)

    }
}

这将确保:

  • (1) 任何无目的地的消息(即消息类型为 MESSAGE 或 SUBSCRIBE 以外的任何消息)都将要求用户进行身份验证
  • (2) 任何人都可以订阅/ user/queue/errors
  • (3) 任何目标以“/app /”开头的消息都将要求用户具有 ROLE_USER 角色
  • (4) 任何以“/user /”或“/topic/friends /”开头且类型为 SUBSCRIBE 的消息都需要 ROLE_USER
  • (5) 拒绝其他任何类型为 MESSAGE 或 SUBSCRIBE 的消息。由于 6,我们不需要这一步,但是它说明了如何在特定的消息类型上进行匹配。
  • (6) 任何其他消息均被拒绝。这是确保您不会错过任何消息的好主意。

Spring Security 还提供XML Namespace支持以保护 WebSocket。可比较的基于 XML 的配置如下所示:

<websocket-message-broker>
    (1)
    <intercept-message type="CONNECT" access="permitAll" />
    <intercept-message type="UNSUBSCRIBE" access="permitAll" />
    <intercept-message type="DISCONNECT" access="permitAll" />

    <intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> (2)
    <intercept-message pattern="/app/**" access="hasRole('USER')" />      (3)

    (4)
    <intercept-message pattern="/user/**" access="hasRole('USER')" />
    <intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />

    (5)
    <intercept-message type="MESSAGE" access="denyAll" />
    <intercept-message type="SUBSCRIBE" access="denyAll" />

    <intercept-message pattern="/**" access="denyAll" /> (6)
</websocket-message-broker>

这将确保:

  • (1) 任何类型为 CONNECT,UNSUBSCRIBE 或 DISCONNECT 的消息都将要求对用户进行身份验证
  • (2) 任何人都可以订阅/ user/queue/errors
  • (3) 任何目标以“/app /”开头的消息都将要求用户具有 ROLE_USER 角色
  • (4) 任何以“/user /”或“/topic/friends /”开头且类型为 SUBSCRIBE 的消息都需要 ROLE_USER
  • (5) 拒绝其他任何类型为 MESSAGE 或 SUBSCRIBE 的消息。由于 6,我们不需要这一步,但是它说明了如何在特定的消息类型上进行匹配。
  • (6) 带有目的地的任何其他消息均被拒绝。这是确保您不会错过任何消息的好主意。

WebSocket 授权说明

为了正确保护您的应用程序,了解 Spring 的 WebSocket 支持非常重要。

WebSocket 对消息类型的授权

了解 SUBSCRIBE 和 MESSAGE 消息类型之间的区别以及它在 Spring 中的工作方式非常重要。

考虑聊天应用程序。

  • 系统可以通过“/topic/system/notifications”的目的地向所有用户发送“ MESSAGE”通知

  • Client 可以通过订阅接收到“/topic/system/notifications”的通知。

虽然我们希望 Client 能够订阅“/topic/system/notifications”,但我们不想让他们将 MESSAGE 发送到该目的地。如果我们允许向“/topic/system/notifications”发送消息,则 Client 端可以直接向该端点发送消息并模拟系统。

通常,应用程序通常会拒绝发送到以broker prefix开头的消息(即“/topic /”或“/queue /”)的任何 MESSAGE。

目标上的 WebSocket 授权

了解目的地是如何转换的也很重要。

考虑聊天应用程序。

  • 用户可以通过将消息发送到“/app/chat”的目的地来向特定用户发送消息。

  • 应用程序会看到该消息,并确保将“ from”属性指定为当前用户(我们不能信任 Client 端)。

  • 然后,应用程序使用SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)将消息发送给收件人。

  • 该消息将变为目标“/queue/user/messages-<sessionid>”

在上面的应用程序中,我们希望允许我们的 Client 端收听“/user/queue”,它被转换为“/queue/user/messages-<sessionid>”。但是,我们不希望 Client 端能够收听“/queue/*”,因为那样会使 Client 端看到每个用户的消息。

通常,应用程序通常会拒绝发送到以broker prefix开头的消息(即“/topic /”或“/queue /”)的任何 SUBSCRIBE。当然,我们可能会提供 exceptions 来说明诸如

Outbound Messages

Spring 包含标题为消息流的部分,描述了消息如何在系统中流动。重要的是要注意,Spring Security 只保护clientInboundChannel。 Spring Security 不会尝试保护clientOutboundChannel

最重要的原因是性能。对于每条传入的消息,通常会有更多的出去消息。我们鼓励保护对端点的订阅,而不是保护出站消息。

10.11.4 强制执行相同来源策略

需要强调的是,浏览器不会对 WebSocket 连接强制执行同源 Policy。这是一个非常重要的考虑因素。

为什么来源相同?

请考虑以下情形。用户访问 bank.com 并验证其帐户。同一用户在其浏览器中打开另一个选项卡,并访问 evil.com。相同来源 Policy 可确保 evil.com 无法读取数据或将数据写入 bank.com。

对于 WebSocket,不适用“相同来源策略”。实际上,除非 bank.com 明确禁止这样做,否则 evil.com 可以代表用户读取和写入数据。这意味着用户可以通过 webSocket 执行任何操作(即转帐),evil.com 可以代表该用户执行操作。

由于 SockJS 尝试模拟 WebSocket,因此它也绕过了相同起源策略。这意味着开发人员在使用 SockJS 时需要明确保护其应用程序不受外部域的影响。

Spring WebSocket 允许的来源

幸运的是,从 Spring 4.1.5 开始,Spring 的 WebSocket 和 SockJS 支持限制了对current domain的访问。 Spring Security 增加了一层额外的保护来提供纵深防御

将 CSRF 添加到 StompHeaders

默认情况下,Spring Security 需要任何 CONNECT 消息类型中的CSRF token。这样可以确保只有有权访问 CSRF 令牌的站点才能连接。由于只有 Same Origin 可以访问 CSRF 令牌,因此不允许外部域进行连接。

通常,我们需要在 HTTPHeaders 或 HTTP 参数中包含 CSRF 令牌。但是,SockJS 不允许使用这些选项。相反,我们必须在 StompHeaders 中包含令牌

应用程序可以通过访问名为_csrf 的请求属性来获取 CSRF 令牌。例如,以下将允许在 JSP 中访问CsrfToken

var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";

如果使用的是静态 HTML,则可以在 REST 端点上公开CsrfToken。例如,以下代码将在 URL/csrf 上显示CsrfToken

@RestController
public class CsrfController {

    @RequestMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}

JavaScript 可以对端点进行 REST 调用,并使用响应填充 headerName 和令牌。

现在,我们可以将令牌包含在 StompClient 端中。例如:

...
var headers = {};
headers[headerName] = token;
stompClient.connect(headers, function(frame) {
  ...

}

禁用 WebSocket 中的 CSRF

如果您想允许其他域访问您的站点,则可以禁用 Spring Security 的保护。例如,在 Java 配置中,您可以使用以下代码:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    ...

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}

10.11.5 使用 SockJS

SockJS提供后备传输以支持较旧的浏览器。使用后备选项时,我们需要放宽一些安全性约束,以允许 SockJS 与 Spring Security 一起使用。

SockJS 和框架选项

SockJS 可以使用利用 iframe 进行运输。默认情况下,Spring Security 将对网站进行denyFramework,以防止 Clickjacking 攻击。为了使基于 SockJS 框架的传输能够正常工作,我们需要配置 Spring Security 以允许相同的来源对内容进行框架化。

您可以使用frame-options元素自定义 X-Frame-Options。例如,以下内容将指示 Spring Security 使用“ X-Frame-Options:SAMEORIGIN”,它允许同一域内的 iframe:

<http>
    <!-- ... -->

    <headers>
        <frame-options
          policy="SAMEORIGIN" />
    </headers>
</http>

同样,您可以使用以下方法自定义框架选项以在 Java 配置中使用相同的来源:

@EnableWebSecurity
public class WebSecurityConfig extends
   WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      // ...
      .headers()
        .frameOptions()
            .sameOrigin();
  }
}

SockJS 和令人放松的 CSRF

SockJS 在 CONNECT 消息上使用 POST 进行任何基于 HTTP 的传输。通常,我们需要在 HTTPHeaders 或 HTTP 参数中包含 CSRF 令牌。但是,SockJS 不允许使用这些选项。相反,我们必须按照“将 CSRF 添加到 StompHeaders 中”部分中的说明在 StompHeaders 中包含令牌。

这也意味着我们需要通过 Web 层放宽对 CSRF 的保护。具体来说,我们要为连接 URL 禁用 CSRF 保护。我们不想禁用每个 URL 的 CSRF 保护。否则,我们的站点将容易受到 CSRF 攻击。

通过提供 CSRF RequestMatcher,我们可以轻松实现这一目标。我们的 Java 配置非常简单。例如,如果我们的踩踏端点是“/chat”,则可以使用以下配置仅对以“/chat /”开头的 URL 禁用 CSRF 保护:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig
    extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            .csrf()
                // ignore our stomp endpoints since they are protected using Stomp headers
                .ignoringAntMatchers("/chat/**")
                .and()
            .headers()
                // allow same origin to frame our site to support iframe SockJS
                .frameOptions().sameOrigin()
                .and()
            .authorizeRequests()

            ...

如果使用基于 XML 的配置,则可以使用[email protected]。例如:

<http ...>
    <csrf request-matcher-ref="csrfMatcher"/>

    <headers>
        <frame-options policy="SAMEORIGIN"/>
    </headers>

    ...
</http>

<b:bean id="csrfMatcher"
    class="AndRequestMatcher">
    <b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
    <b:constructor-arg>
        <b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
          <b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
            <b:constructor-arg value="/chat/**"/>
          </b:bean>
        </b:bean>
    </b:constructor-arg>
</b:bean>

[6]请注意,您需要在应用程序上下文 XML 文件中包括安全名称空间,才能使用此语法。仍支持使用filter-chain-map的旧语法,但不推荐使用,而推荐使用构造函数自变量注入。

[7]代替路径模式,可以使用request-matcher-ref属性指定RequestMatcher实例,以实现更强大的匹配

[8]当浏览器不支持 cookie 且将jsessionid参数附加到分号后的 URL 时,您可能已经看到了。但是,RFC 允许这些参数出现在 URL 的任何路径段中

[9]一旦请求离开FilterChainProxy,将返回原始值,因此应用程序仍然可以使用它们。

[10]因此,例如,原始请求路径/secure;hack=1/somefile.html;hack=2将返回为/secure/somefile.html

[11]我们使用转发,以便 SecurityContextHolder 仍包含主体的详细信息,这可能对显示给用户很有用。在旧版本的 Spring Security 中,我们依靠 servlet 容器来处理 403 错误消息,该消息缺少此有用的上下文信息。

[12]在 Spring Security 2.0 和更早的版本中,此过滤器称为HttpSessionContextIntegrationFilter,并且存储上下文的所有工作都是由过滤器本身完成的。如果您熟悉此类,那么现在可以在HttpSessionSecurityContextRepository上找到大多数可用的配置选项。

[13]由于历史原因,在 Spring Security 3.0 之前,此过滤器称为AuthenticationProcessingFilter,入口点称为AuthenticationProcessingFilterEntryPoint。由于该框架现在支持许多不同形式的身份验证,因此在 3.0 中都为它们都指定了更具体的名称。

[14]在 3.0 之前的版本中,此时的应用程序流程已 Developing 到由此类和策略插件上的属性混合控制的阶段。决定使用 3.0 重构代码以使这两种策略完全负责。

[15]如果DigestAuthenticationFilter.passwordAlreadyEncoded设置为true,则可以将密码编码为 HEX(MD5(username:realm:password))格式。但是,其他密码编码将不适用于摘要身份验证。

[16]本质上,用户名不包含在 cookie 中,以防止不必要地公开有效的登录名。本文的 Comment 部分对此进行了讨论。

[17]通过在身份验证后执行重定向的机制(例如,表单登录)进行身份验证将不会被SessionManagementFilter检测到,因为在身份验证请求期间不会调用过滤器。在这种情况下,必须单独处理会话 Management 功能。

[18]使用key属性不应视为在此提供任何实际的安全性。这只是一个簿记练习。如果在可能进行身份验证的 Client 端构造Authentication对象(例如使用 RMI 调用)的情况下共享ProviderManager(其中包含AnonymousAuthenticationProvider),则恶意 Client 端可以提交自己创建的AnonymousAuthenticationToken(选择用户名和权限列表)。如果key是可猜测的或可以找到,则令牌将被匿名提供者接受。正常使用情况下这不是问题,但是如果您使用的是 RMI,则最好使用自定义的ProviderManager,它会省略匿名提供程序,而不是共享用于 HTTP 身份验证机制的提供程序。