本节讨论 Spring Security 的跨站请求伪造(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 现在假装你认证你的银行网站,然后,没有登出,访问一个恶意的网站。恶意网站包含一个 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 自动化的。这意味着你甚至不需要点击这个按钮。那么我们如何保护自己免受这种攻击呢? 问题是,来自银行网站的 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 参数的正确值(必须在恶意网站上明确提供),并且当服务器将实际令牌与预期令牌进行比较时,传输将失败。 什么时候使用 CSRF 保护?我们的建议是使用 CSRF 保护任何可以由浏览器处理正常用户的请求。如果你只创建非浏览器客户端使用的服务,那么你可能希望禁用 CSRF 保护。 一个常见的问题是:“我需要保护 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" } 如果应用程序没有验证内容类型,那么它将暴露于此漏洞。根据设置,仍然可以通过更新 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> 如果我的应用程序是无状态的呢?这并不一定意味着你受到了保护。事实上,如果用户对于给定的请求不需要在 web 浏览器中执行任何操作,那么他们可能仍然容易受到 CSRF 攻击。 例如,考虑一个应用程序使用一个自定义 cookie,其中包含用于身份验证的所有状态,而不是 JSESSIONID。当进行 CSRF 攻击时,将按照与前面示例中发送 JSESSIONID cookie 相同的方式发送带有请求的自定义 cookie。 使用基本身份验证的用户也容易受到 CSRF 攻击,因为浏览器将以与前面示例中发送 JSESSIONID cookie 相同的方式在任何请求中自动包括用户名密码。 那么,使用 Spring Security 来保护我们的站点不受 CSRF 攻击的步骤是什么呢?使用 Spring Security 的 CSRF 保护的步骤概述如下: 防止 CSRF 攻击的第一步是确保你的网站使用适当的 HTTP 动词。具体地说,在使用 Spring Security 的 CSRF 支持之前,你需要确定你的应用程序正在对任何修改状态的东西使用 PATCH、POST、PUT 和/或 DELETE。 这不是 Spring Security 支持的限制,而是对适当的 CSRF 预防的一般要求。原因是在 HTTP GET 中包含私有信息会导致信息泄漏。关于敏感信息的使用 POST 而不是 GET 的一般指南,请参阅 RFC 2616 小节 15.1.3 在 URI 中编码敏感信息。
下一步是在应用程序中包含 Spring Security 的 CSRF 保护。有些框架通过无效用户会话来处理无效的 CSR F令牌,但这会导致其自身的问题。默认情况下,Spring Security 的 CSRF 保护将产生拒绝 HTTP 403 访问。这可以通过配置 AccessDeniedHandler 以不同的方式处理 在 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 令牌包含在所有 PATCH、POST、PUT 和 DELATE 方法中。解决这一问题的一种方法是使用 <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 标签。
如果使用 JSON,则不可能在 HTTP 参数中提交 CSRF 令牌。相反,你可以在 HTTP 报头中提交令牌。一个典型的模式是在你的元标签中包含 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 标签,而不是手动创建元标签。 然后可以在所有 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 的 rest.js。rest.js 模块提供了以 RESTful 方式处理 HTTP 请求和响应的高级支持。核心能力是根据需要通过将拦截器链接到客户端来,将 HTTP 客户端添加到行为的能力。 var client = rest.chain(csrf, { token: $("meta[name='_csrf']").attr("content"), name: $("meta[name='_csrf_header']").attr("content") }); 配置的客户端可以与需要向 CSRF 保护资源请求的应用程序的任何组件共享。rest.js 和 jQuery 之间的一个显著区别是,只有配置客户端的请求将包含 CSRF 令牌,相比 jQuery,所有请求都将包含令牌。请求接收令牌的范围的能力有助于防止将 CSRF 令牌泄漏给第三方。有关的更多信息信息请参阅 rest.js 参考文档。
有些情况下,用户希望在 cookie 中保留
你可以使用以下方式在 XML 中配置 <http> <!-- ... --> <csrf token-repository-ref="tokenRepository"/> </http> <b:bean id="tokenRepository" class="org.springframework.security.web.csrf.CookieCsrfTokenRepository" p:cookieHttpOnly="false"/>
你可以使用 Java 配置来配置 @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } }
在实现 CSRF 时有一些注意事项。
一个问题是,预期的 CSRF 令牌存储在 HttpSession 中,所以一旦 HttpSession 期满,配置的
减轻活动用户超时的简单方法是使用一些J avaScript,让用户知道他们的会话即将到期。用户可以单击一个按钮来继续并刷新会话。
或者,指定自定义 最后,应用程序可以被配置为使用不会过期的 CookieCsrfTokenRepository。如前所述,这并不像使用会话那样安全,但在许多情况下可能是足够好的。
为了防止伪造登录请求,登录表单也应该受到保护,防止 CSRF 攻击。由于
保护表单登录的常用技术是使用 JavaScript 函数在表单提交之前获得有效的 CSRF 令牌。通过这样做,不需要考虑会话超时(在上一节中讨论),因为会话是在表单提交之前创建的(假设没有配置 CookieCsrfTokenRepository),所以用户可以停留在登录页上并当他想要的时候提交用户名/密码,为了实现这一点,你可以利用 Spring Security 提供的 仅使用 HTTP POST 时添加 CSRF 将更新 LogoutFilter。这确保注销需要 CSRF 令牌,并且恶意用户不能强制注销你的用户。 一种方法是使用表单注销。如果你真的想要一个链接,你可以使用 JavaScript 让链接执行一个 POST(也就是说,在一个隐藏的表单上)。对于禁用 JavaScript 的浏览器,可以选择让链接将用户带到将执行 POST 的注销确认页面。 如果你真的想使用注销的 HTTP GET,你可以这样做,但是记住这是不推荐的。例如,下面的 Java 配置将执行注销,URL /logout 是用任何 HTTP 方法请求的: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")); } } 使用 multipart/form-data 时使用 CSRF 保护有两种选择。每个选项都有它的折衷。
第一个选项是确保在 Spring Security 过滤器之前指定
为了确保在使用 Java 配置的 Spring Security 过滤器之前指定 public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer { @Override protected void beforeSpringSecurityFilterChain(ServletContext servletContext) { insertFilters(servletContext, new MultipartFilter()); } }
为了确保在具有 XML 配置的 Spring Security 过滤器之前指定 <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>
如果允许未经授权的用户上传临时文件是不可接受的,另一种方法是将 <form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data"> 这种方法的缺点是查询参数可能泄漏。更确切地说,将敏感数据放置在身体或报头中以确保其不泄漏是公认的最佳做法。附加信息可以在 RFC 2616 小节 15.1.3 中找到 URI 中敏感信息的编码。 Spring Security 的目标是提供保护用户免受漏洞攻击的默认值。这并不意味着你被迫接受它的所有默认值。
例如,可以提供自定义的 CsrfTokenRepository 来重写存储
还可以指定自定义 RequestMatcher 来确定哪些请求受 CSRF 保护(即,也许你不关心是否利用了注销)。简而言之,如果 Spring Security 的 CSRF 保护不完全符合你的要求,就可以自定义行为。有关如何使用 XML 请参阅 小节 43.1.19, “<csrf>” 文档,以及了解如何在使用 Java 配置时进行这些自定义的细节请参阅
Spring Framework 为 CORS 提供了一流的支持。CORS 必须在 Spring Security 之前进行处理,因为预检请求将不包含任何 cookies(即
确保 CORS 首先处理的最简单的方法是使用 @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 支持,可以省略指定 @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> |