9.3.3 防止跨站请求伪造

我们可以回忆一下,当一个 POST 请求提交到 “/spittles” 上时,SpittleController 将会为用户创建一个新的 Spittle 对象。但是,如果这个 POST 请求来源于其他站点的话,会怎么样呢?如果在其他站点提交如下表单,这个 POST 请求会造成什么样的结果呢?

<form method="POST" action="http://www.spittr.com/spittles">
  <input type="hidden" name="message" value="I'm stupid!" />
  <input type="submit" value="Click here to win a new car!" />
</form>

假设你禁不住获得一辆新汽车的诱惑,点击了按钮 —— 那么你将会提交表单到如下地址 http://www.spittr.com/spittles。如果你已经登录到了 spittr.com,那么这就会广播一条消息,让每个人都知道你做了一件蠢事。

这是跨站请求伪造(cross-site request forgery,CSRF)的一个简单样例。简单来讲,如果一个站点欺骗用户提交请求到其他服务器的话,就会发生 CSRF 攻击,这可能会带来消极的后果。尽管提交 “I’m stupid!” 这样的信息到微博站点算不上什么 CSRF 攻击的最糟糕场景,但是你可以很容易想到更为严重的攻击情景,它可能会对你的银行账号执行难以预期的操作。

从 Spring Security 3.2 开始,默认就会启用 CSRF 防护。实际上,除非你采取行为处理 CSRF 防护或者将这个功能禁用,否则的话,在应用中提交表单时,你可能会遇到问题。Spring Security 通过一个同步 token 的方式来实现 CSRF 防护的功能。它将会拦截状态变化的请求(例如,非 GET、HEAD、OPTIONS 和 TRACE 的请求)并检查 CSRF token。如果请求中不包含 CSRF token 的 话,或者 token 不能与服务器端的 token 相匹配,请求将会失败,并抛出 CsrfException 异常。

这意味着在你的应用中,所有的表单必须在一个 “_csrf” 域中提交 token,而且这个 token 必须要与服务器端计算并存储的 token 一致,这样的话当表单提交的时候,才能进行匹配。

好消息是,Spring Security 已经简化了将 token 放到请求的属性中这一任务。如果你使用 Thymeleaf 作为页面模板的话,只要 <form> 标签的 action 属性添加了 Thymeleaf 命名空间前缀,那么就会自动生成一 个 “_csrf” 隐藏域:

<form method="POST" th:action="@{/spittles}">
  ...
</form>

如果使用 JSP 作为页面模板的话,我们要做的事情非常类似:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />

更好的功能是,如果使用 Spring 的表单绑定标签的话,标签会自动为我们添加隐藏的 CSRF token 标签。

处理 CSRF 的另外一种方式就是根本不去处理它。我们可以在配置中通过调用 csrf().disable() 禁用 Spring Security 的 CSRF 防护功能, 如下所示:

程序清单 9.6 我们可以禁用 Spring Security 的 CSRF 防护功能
@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    ...
    .csrf()
    .disable();
}

需要提醒的是,禁用 CSRF 防护功能通常来讲并不是一个好主意。如果这样做的话,那么应用就会面临 CSRF 攻击的风险。只有在深思熟虑之后,才能使用程序清单 9.6 中的配置。

我们已经配置好了用户存储,也配置好了使用 Spring Security 来拦截请求,那么接下来就该提示用户输入凭证了。

Last updated