Rails Rails 7 Turbo 环境集成 reCAPTCHA 的方法

Rei · 2022年08月09日 · 最后由 canonpd 回复于 2023年10月15日 · 601 次阅读

原文发布在 https://geeknote.net/Rei/posts/1432


最近有人反馈 GeekNote 的注册流程体验很差(#23),我调试之后发现之前集成 reCAPTCHA 的代码有错,会导致验证经常失败。解决的过程记录如下。

问题

reCAPTCHA 是 Google 提供的验证码服务,Rails 有一个 Gem recaptcha 可以帮助集成。按照这个 Gem 的文档,在需要验证码的地方只需加入以下代码:

<%= form_for @foo do |f| %>
  # …
  <%= recaptcha_tags %>
  # …
<% end %>

recaptcha_tags 插入的内容如下:

<script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="<%= ENV['RECAPTCHA_SITE_KEY'] %>"></div>
<noscript>
  ...
  <textarea name="g-recaptcha-response"></textarea>
</noscirpt>

在访问页面时,api.js会在 <head> 插入另一段 <script>,这个 <script> 才是真正执行代码。在没有 Turbo 时它可以正常工作,但是在 Turbo 开启的情况下会发生以下错误:

  • 由于 Turbo 的页面间 <head> 内容会被保留,导致多次访问有验证码的页面时产生重复执行。
  • 由于代码重复执行,过程会抛出错误,<noscript> 的内容没有被正确处理,导致提交空的 g-recaptcha-response 参数。
  • 空的 g-recaptcha-response 参数导致验证失败。

以上错误已经提交了 Issues(#47),但是还没被官方解决。

那么要如何正确集成 reCAPTCHA 呢?

解决方案

首先要解决重复执行的问题,由于 Turbo 环境 <head> 会被保留,所以不希望重复执行的代码应该放到 <head> 里。

layouts/application.html.erb 加入以下代码:

<head>
  ...
  <script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
</head>

提示:如果想选择性载入 reCAPTCHA,可以用 yield :headcontent_for :head 的组合仅在需要的页面插入 script。

接着,在需要验证码的地方加入以下内容:

<%= form_for @foo do |f| %>
  # …
  <div class="g-recaptcha" data-sitekey="<%= ENV['RECAPTCHA_SITE_KEY'] %>" data-controller="recaptcha"></div>
  # …
<% end %>

如果这个页面是访问的首个页面,recaptcha/api.js 在加载后会自动查找包含 .g-recaptcha 的元素进行渲染。但如果是通过 Turbo 访问的页面,recaptcha/api.js 的渲染逻辑不会执行,所以这里需要一个 Stimulus 控制器进行处理。

在 Stimulus 控制器目录创建以下代码:

// recaptcha_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    // Skip this if grecaptcha has not been loaded or has already been rendered.
    if (window.grecaptcha && window.grecaptcha.render && this.element.childElementCount == 0) {
      grecaptcha.render(this.element, {
        'sitekey' : this.element.dataset.sitekey
      })
    }
  }
}

该控制器判断,如果 recaptcha api 已经加载,并且元素本身还没被 recaptcha 渲染,则调用 grecaptcha.render 进行渲染。这样它就能处理 Turbo 环境下验证码的初始化。

经过以上修改后,不再出现 api.js 重复加载,也不会出现提交空的 g-recaptcha-response 参数的情况,问题解决。

🎉 谢谢分享

如果有 turbo 但没有 stimulus 的情况,可以考虑

document.addEventListener("turbo:load", function() 
document.addEventListener("turbo:frame-load", function(ev)

来做初始化

很重要的原因是 Turbo Stream 返回内容中的 <script> 不会被执行。现在最新版本已经修复这个问题了。 https://github.com/hotwired/turbo/pull/660

https://github.com/ambethia/recaptcha/wiki/Recaptcha-with-Turbo-and-Stimulus

有新的玩法了,使用 Turbo 结合 Stimulus。recaptcha v3 无感先试,不行再切到 v2 让用户点。 Rails 7.1 已尝试集成了,没有发现问题。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号