文档

§防止跨站请求伪造

跨站请求伪造 (CSRF) 是一种安全漏洞,攻击者利用它欺骗受害者的浏览器使用受害者的会话发出请求。由于会话令牌会随每个请求一起发送,如果攻击者能够强制受害者的浏览器代表他们发出请求,攻击者就可以代表用户发出请求。

建议您熟悉 CSRF,了解攻击向量是什么,以及攻击向量不是什么。我们建议从 OWASP 的此信息 开始。

对于哪些请求是安全的以及哪些容易受到 CSRF 请求的攻击,没有简单的答案;原因是,对于插件和未来规范扩展允许什么,没有明确的规范。从历史上看,浏览器插件和扩展放宽了框架以前认为可以信任的规则,为许多应用程序引入了 CSRF 漏洞,框架需要承担修复这些漏洞的责任。出于这个原因,Play 在其默认设置中采取了保守的方法,但允许您精确配置何时进行检查。默认情况下,Play 将在以下所有条件都为真时要求进行 CSRF 检查

注意:如果您使用基于浏览器的身份验证,而不是使用 cookie 或 HTTP 身份验证,例如 NTLM 或基于客户端证书的身份验证,那么您必须设置 play.filters.csrf.header.protectHeaders = null,这将保护所有请求,或者将身份验证中使用的标头包含在 protectHeaders 中。

§Play 的 CSRF 防护

Play 支持多种方法来验证请求是否为 CSRF 请求。主要机制是 CSRF 令牌。此令牌放置在提交的每个表单的查询字符串或正文中,以及用户的会话中。然后,Play 验证这两个令牌是否存在且匹配。

为了对非浏览器请求提供简单的保护,Play 默认情况下会检查带有 CookieAuthorization 标头的请求。您可以配置 play.filters.csrf.header.protectHeaders 来定义必须存在的标头才能执行 CSRF 检查。如果您使用 AJAX 发出请求,则可以将 CSRF 令牌放置在 HTML 页面中,然后使用 Csrf-Token 标头将其添加到请求中。

或者,您可以设置 play.filters.csrf.header.bypassHeaders 来匹配常见的标头:常见的配置是

此配置将如下所示

play.filters.csrf.header.bypassHeaders {
  X-Requested-With = "*"
  Csrf-Token = "nocheck"
}

使用此配置选项时应谨慎,因为历史上浏览器插件破坏了这种类型的 CSRF 防御。

§信任 CORS 请求

默认情况下,如果您在 CSRF 过滤器之前有一个 CORS 过滤器,则 CSRF 过滤器将允许来自受信任来源的 CORS 请求通过。要禁用此检查,请设置配置选项 play.filters.csrf.bypassCorsTrustedOrigins = false

§应用全局 CSRF 过滤器

注意:从 Play 2.6.x 开始,CSRF 过滤器包含在 Play 的默认过滤器列表中,这些过滤器会自动应用于项目。有关更多信息,请参阅 过滤器页面

Play 提供了一个全局 CSRF 过滤器,可以将其应用于所有请求。这是向应用程序添加 CSRF 防护的最简单方法。要手动添加过滤器,请将其添加到 application.conf

play.filters.enabled += "play.filters.csrf.CSRFFilter"

您也可以在路由文件中为特定路由禁用 CSRF 过滤器。为此,在您的路由之前添加 `nocsrf` 修饰符标签。

+ nocsrf
POST  /api/new              controllers.Api.newThing

§使用隐式请求

所有 CSRF 功能都假设在隐式作用域中存在一个隐式 RequestHeader(或一个 Request,它扩展了 RequestHeader),如果没有,则无法编译。以下将显示示例。

§在操作中定义隐式请求

对于所有需要访问 CSRF 令牌的操作,必须使用 `implicit request =>` 隐式公开请求,如下所示

// this actions needs to access CSRF token
def someMethod: Action[AnyContent] = Action { implicit request =>
  // access the token as you need
  Ok
}

这是因为像 CSRF.getToken 这样的辅助方法访问接收请求作为隐式参数来检索 CSRF 令牌,例如

def someAction: Action[AnyContent] = Action { implicit request =>
  accessToken // request is passed implicitly to accessToken
  Ok("success")
}

def accessToken(implicit request: Request[_]) = {
  val token = CSRF.getToken // request is passed implicitly to CSRF.getToken
}

§在方法之间传递隐式请求

如果您将代码分解为使用 CSRF 功能的方法,那么您可以从操作中传递隐式请求。

def action: Action[AnyContent] = Action { implicit request =>
  anotherMethod("Some para value")
  Ok
}

def anotherMethod(p: String)(implicit request: Request[_]) = {
  // do something that needs access to the request
}

§在模板中定义隐式请求

您的 HTML 模板应该有一个隐式 RequestHeader 参数传递给您的模板,如果它还没有,因为 CSRF.formField 助手需要一个传递进来(下面将详细讨论)。

@(...)(implicit request: RequestHeader)

由于您通常会将 CSRF 与需要 MessagesProvider 实例的表单助手一起使用,您可能希望使用 MessagesAbstractController 或其他提供 MessagesRequestHeader 的控制器。

@(...)(implicit request: MessagesRequestHeader)

或者,如果您使用的是具有 I18nSupport 的控制器,您可以将消息作为单独的隐式参数传递。

@(...)(implicit request: RequestHeader, messages: Messages)

§获取当前令牌

可以使用 CSRF.getToken 方法访问当前 CSRF 令牌。它接受一个隐式 RequestHeader,因此请确保作用域中有一个。

val token: Option[CSRF.Token] = CSRF.getToken

注意:如果安装了 CSRF 过滤器,Play 将尝试避免生成令牌,只要使用的 cookie 是 HttpOnly(这意味着它无法从 JavaScript 访问)。当发送具有严格主体的响应时,Play 会跳过将令牌添加到响应中,除非已经调用了 `CSRF.getToken`。这对于不需要 CSRF 令牌的响应来说,可以显著提高性能。如果 cookie 未配置为 HttpOnly,Play 将假设您希望从 JavaScript 访问它,并无论如何都会生成它。

如果您没有使用 CSRF 过滤器,您也应该注入 CSRFAddTokenCSRFCheck 操作包装器,以强制在特定操作上添加令牌或 CSRF 检查。否则,令牌将不可用。

import play.api.mvc._
import play.api.mvc.Results._
import play.filters.csrf._
import play.filters.csrf.CSRF.Token

class CSRFController(components: ControllerComponents, addToken: CSRFAddToken, checkToken: CSRFCheck)
    extends AbstractController(components) {
  def getToken =
    addToken(Action { implicit request =>
      val Token(name, value) = CSRF.getToken.get
      Ok(s"$name=$value")
    })
}

为了帮助将 CSRF 令牌添加到表单中,Play 提供了一些模板助手。第一个将它添加到操作 URL 的查询字符串中

@import helper._

@form(CSRF(routes.ItemsController.save())) {
    ...
}

这可能会呈现一个看起来像这样的表单

<form method="POST" action="/items?csrfToken=1234567890abcdef">
   ...
</form>

如果在查询字符串中使用令牌不可取,Play 还提供了一个助手,用于将 CSRF 令牌作为隐藏字段添加到表单中

@form(routes.ItemsController.save()) {
    @CSRF.formField
    ...
}

这可能会呈现一个看起来像这样的表单

<form method="POST" action="/items">
   <input type="hidden" name="csrfToken" value="1234567890abcdef"/>
   ...
</form>

§将 CSRF 令牌添加到会话中

为了确保 CSRF 令牌可用于在表单中呈现,并发送回客户端,全局过滤器将为所有接受 HTML 的 GET 请求生成一个新令牌,如果传入请求中没有令牌。

§在每个操作的基础上应用 CSRF 过滤

有时全局 CSRF 过滤可能不合适,例如在应用程序可能希望允许某些跨域表单发布的情况下。一些非基于会话的标准,例如 OpenID 2.0,需要使用跨站点表单发布,或在服务器到服务器 RPC 通信中使用表单提交。

在这些情况下,Play 提供了两个可以与您的应用程序操作组合的操作。

第一个操作是 CSRFCheck 操作,它执行检查。它应该添加到所有接受会话身份验证的 POST 表单提交的操作中

import play.api.mvc._
import play.filters.csrf._

def save = checkToken {
  Action { implicit req =>
    // handle body
    Ok
  }
}

第二个操作是 CSRFAddToken 操作,它在传入请求中不存在时生成 CSRF 令牌。它应该添加到所有呈现表单的操作中

import play.api.mvc._
import play.filters.csrf._

def form = addToken {
  Action { implicit req => Ok(views.html.itemsForm) }
}

应用这些操作的一种更便捷的方法是将它们与 Play 的 操作组合 结合使用

import play.api.mvc._
import play.filters.csrf._

class PostAction @Inject() (parser: BodyParsers.Default) extends ActionBuilderImpl(parser) {
  override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    // authentication code here
    block(request)
  }

  override def composeAction[A](action: Action[A]) = checkToken(action)
}

class GetAction @Inject() (parser: BodyParsers.Default) extends ActionBuilderImpl(parser) {
  override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    // authentication code here
    block(request)
  }

  override def composeAction[A](action: Action[A]) = addToken(action)
}

然后,您可以最大限度地减少编写操作所需的样板代码

def save: Action[AnyContent] = postAction {
  // handle body
  Ok
}

def form: Action[AnyContent] = getAction { implicit req => Ok(views.html.itemsForm) }

§CSRF 配置选项

可以在过滤器 reference.conf 中找到完整的 CSRF 配置选项范围。一些示例包括

§使用编译时依赖注入的 CSRF

如果您的应用程序使用编译时依赖注入,则可以使用上述所有功能。您可以将特性 CSRFComponents 混合到您的应用程序组件蛋糕中,以帮助进行连接。有关编译时依赖注入的更多详细信息,请参阅 相关文档页面

§测试 CSRF

渲染时,您可能需要将 CSRF 令牌添加到模板中。您可以使用 import play.api.test.CSRFTokenHelper._ 来完成此操作,它会用 withCSRFToken 方法丰富 play.api.test.FakeRequest

import play.api.test.CSRFTokenHelper._
import play.api.test.FakeRequest
import play.api.test.Helpers._
import play.api.test.WithApplication

class UserControllerSpec extends Specification {
  "UserController GET" should {
    "render the index page from the application" in new WithApplication() {
      override def running() = {
        val controller = app.injector.instanceOf[UserController]
        val request    = FakeRequest().withCSRFToken
        val result     = controller.userGet().apply(request)

        status(result) must beEqualTo(OK)
        contentType(result) must beSome("text/html")
      }
    }
  }
}

下一步: 自定义验证


发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区开始对话。