Turnstile 校验

Odoo 网站防机器人为什么不是前端挂个验证码就够:website_cf_turnstile 的挂载、服务端校验与失败回退讲透

很多人看到 website_cf_turnstile 只有几段前端脚本和一个 ir.http 扩展,就以为它只是给表单塞了个 Cloudflare 小组件。其实它真正关键的地方,是把 site key 注入前端、把提交按钮先锁住、再在控制器统一走 _verify_request_recaptcha_token 校验,并把各种失败状态翻译成可控异常。

前端 框架 网站
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 7 阅读

很多人第一次看 website_cf_turnstile,会下一个过于轻松的判断:

不就是在表单前面插一个 Cloudflare Turnstile 小组件吗?

这判断只对了一半。

/home/ubuntu/odoo-temp/addons/website_cf_turnstile 的实现看,官方真正想解决的不是“页面上有没有验证码”,而是:

  • 前端什么时候自动挂载挑战组件;
  • 用户没通过验证前,提交按钮怎么先锁住;
  • 控制器收到请求后,怎么在服务端统一验 token;
  • 配置错误、token 过期、action 不匹配时,系统该抛什么错误。

所以它的本质不是一个 UI 装饰,而是一条 “前端挂件 + 服务端判定” 的完整链路。

一、Turnstile 先不是“默认全站开启”,而是先把 site key 暴露给前端

models/ir_http.py 里,模块重写了 get_frontend_session_info()

它做的事很简单:

  • ir.config_parameter 里读取 cf.turnstile_site_key
  • 如果有值,就把它塞进前端 session 信息里的 turnstile_site_key

这一步很关键,因为前端脚本并不会硬编码站点 key。

也就是说,浏览器端是否会真正渲染 Turnstile,前提不是“模块装没装”,而是:

  1. 后台是否配置了 site key;
  2. 前端 session 里是否因此拿到了 session.turnstile_site_key

这也是为什么有些人装了模块却觉得“前端没反应”——不是脚本坏了,而是前端压根没拿到可以渲染的 key。

二、它不是只接管一种表单,而是同时照顾网站表单和通用 data-captcha 表单

前端部分有两条入口:

1)form.js:补丁网站表单片段

它 patch 了 @website/snippets/s_website_form/formForm.prototype.start()

逻辑是:

  • 先清理旧的 Turnstile DOM;
  • 如果表单 没有 s_website_form_no_recaptcha
  • 且当前表单还没有 .s_turnstile
  • 且 session 里有 turnstile_site_key
  • 就在发送按钮前面插入 Turnstile 容器。

这说明网站构建器里那种标准 s_website_form 片段,会被自动接管。

2)turnstile_captcha.js:处理 form[data-captcha]

除了标准网站表单,模块还注册了一个 public interaction,专门匹配 form[data-captcha]

这意味着另一些自定义表单,只要走了 data-captcha 约定,也能挂进同样的校验链。

所以这里不是“一个组件写死给一个页面”,而是官方把 Turnstile 做成了 统一验证码接入层

三、真正容易被忽略的,不是渲染,而是“先禁用提交按钮”

turnstile.js 里有一个很实用的细节:

  • 创建组件时,会先找到提交按钮;
  • TurnStile.disableSubmit() 给按钮加上 disabledcf_form_disabled
  • 只有 Cloudflare 回调 turnstileSuccess 触发后,才把这些 class 移掉。

同时它还塞了一个隐藏输入:turnstile_captcha_valid,并把它标成 required

这两个动作合起来,解决了两个真实问题:

  1. 用户还没完成挑战,就不要让表单误提交
  2. 密码管理器或浏览器自动填充,不应该绕过人工验证直接提交

很多自研验证码只想着“把 token 带上”,却没处理提交时机。Odoo 这里反而做得比较稳:

先锁按钮,成功后再放行。

四、前端看到组件,不代表后端已经安全;真正的判定发生在 _verify_request_recaptcha_token()

最值得看的地方在 website_cf_turnstile/models/ir_http.py

它没有另造一套新入口,而是直接复用了 Odoo 原有的验证码校验钩子:

  • super()._verify_request_recaptcha_token(action)
  • 再自己读取 request.params 里的 turnstile_captcha
  • 再调用 _verify_turnstile_token(ip_addr, token, action)

这个设计很聪明,因为 google_recaptcha 也扩展的正是同一个方法。

换句话说,Odoo 的思路不是“每种验证码都让控制器单独写一套 if/else”,而是:

控制器只管在合适时机调用 _verify_request_recaptcha_token(action);至于背后接的是 Google 还是 Cloudflare,由扩展模块接管。

这也是开发里最容易误解的一点:

如果你的控制器根本没调用 _verify_request_recaptcha_token(),前端 Turnstile 再漂亮也只是摆设。

组件只负责拿 token,是否真正拦请求,在服务端

五、action 不是装饰字段,而是防“拿旧 token 乱贴”的边界

_verify_turnstile_token() 调 Cloudflare siteverify 接口后,不只看 success

它还会检查:

  • 这次 token 的 action
  • 当前服务端要求验证的 action
  • 二者是否一致。

如果不一致,就返回 wrong_action

这背后的意思很实际:

  • website_form 生成的 token,不应该随便拿去冒充别的表单;
  • website_event_registration 的 token,也不该混到 newsletter 订阅里用。

所以 action 的作用不是做展示,而是在服务端给每类表单加一层“来源语义”。

六、失败并不只有“机器人”这一种,Odoo 把错误分成了几层

源码里把 Cloudflare 返回值映射成一组业务状态:

  • no_secret:后台没配 secret,视为未启用;
  • wrong_secret:secret 错了;
  • wrong_token:token 缺失或非法;
  • timeout:超时或 token 过旧;
  • bad_request:请求格式不对;
  • wrong_action:token 不是为当前动作生成的;
  • 其他失败:按可疑行为处理。

然后 _verify_request_recaptcha_token() 再把这些状态翻译成:

  • ValidationError
  • UserError

这样一来,配置问题、用户重试问题、恶意流量问题就不会全都塌成一句“验证失败”。

这也是生产环境里很重要的一个边界:

验证系统不能只告诉你“错了”,还要告诉你 错在哪一层

七、和 google_recaptcha 对照看,会更容易理解它的定位

website_cf_turnstile 基本沿用了 google_recaptcha 的框架:

  • 都往 session 里注入前端公钥;
  • 都重写 _verify_request_recaptcha_token()
  • 都在服务端发一次第三方验证请求;
  • 都把结果映射成 Odoo 自己的异常。

但二者也有明显差异:

  • Google reCAPTCHA v3 还会看 score;
  • Turnstile 这里更偏向 challenge 成败与 action 对齐;
  • Turnstile 使用 turnstile_captcha 参数名,而不是 recaptcha_token_response

所以你可以把它理解成:

Odoo 没有重做一套验证码框架,而是在已有验证码钩子上,换了一个 Cloudflare 适配器。

八、实战里最常见的 4 个误区

1)前端出现了 Turnstile,就等于已经安全

不对。控制器没调用 _verify_request_recaptcha_token(),那只是装了个前端门铃,门根本没锁。

2)没有 secret 时应该报错

源码不是这么设计的。no_secret 被视为未激活,而不是验证失败。

这说明官方更想把“未配置”当成可控关闭,而不是把所有表单都打死。

3)只要 token 有效,就能跨表单复用

不对。action 不匹配会被当成 wrong_action

4)验证码失败就一定是机器人

也不对。超时、私钥错误、请求格式问题、前端没正确带 token,都可能失败。

总结

website_cf_turnstile 真正有价值的地方,不是“把 Cloudflare 组件嵌进 Odoo 页面”,而是它把验证码做成了一条完整的提交防线:

  • 先通过 session 把 site key 送到前端;
  • 再自动给标准网站表单和 data-captcha 表单挂组件;
  • 在挑战成功前锁住提交按钮;
  • 最后由服务端 _verify_request_recaptcha_token() 真正判定请求能不能过。

如果只记一句,可以记这句:

在 Odoo 里,Turnstile 不是“前端验证码组件”,而是“前端拿 token、后端做裁决”的表单安全接入层。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。