很多人第一次看 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,前提不是“模块装没装”,而是:
- 后台是否配置了 site key;
- 前端 session 里是否因此拿到了
session.turnstile_site_key。
这也是为什么有些人装了模块却觉得“前端没反应”——不是脚本坏了,而是前端压根没拿到可以渲染的 key。
二、它不是只接管一种表单,而是同时照顾网站表单和通用 data-captcha 表单
前端部分有两条入口:
1)form.js:补丁网站表单片段
它 patch 了 @website/snippets/s_website_form/form 的 Form.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()给按钮加上disabled和cf_form_disabled; - 只有 Cloudflare 回调
turnstileSuccess触发后,才把这些 class 移掉。
同时它还塞了一个隐藏输入:turnstile_captcha_valid,并把它标成 required。
这两个动作合起来,解决了两个真实问题:
- 用户还没完成挑战,就不要让表单误提交;
- 密码管理器或浏览器自动填充,不应该绕过人工验证直接提交。
很多自研验证码只想着“把 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() 再把这些状态翻译成:
ValidationErrorUserError
这样一来,配置问题、用户重试问题、恶意流量问题就不会全都塌成一句“验证失败”。
这也是生产环境里很重要的一个边界:
验证系统不能只告诉你“错了”,还要告诉你 错在哪一层。
七、和 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
评论区