uniapp 中使用cloudflare的turnstile人机验证

uniapp 中使用cloudflare的turnstile人机验证

主要用到的uniapp中webview

链接地址

主要用到的webview方法
  • @message 网页向应用 postMessage 时,会在特定时机(后退、组件销毁、分享)触发并收到消息。( H5 暂不支持(可以直接使用 window.postMessage))
  • 应用向网页url传值
  • uni.postMessage 向应用发送消息 (抖音小程序不支持、H5 暂不支持(可以直接使用 window.postMessage))
  • UniAppJSBridgeReady 把uni.webview.1.5.5.js 下载到自己的服务器后, 待触发 UniAppJSBridgeReady 事件后,即可调用 uni 的 API
  • turnstile.ready函数配置对象,turnstile.ready(function () {}),可按照文档配置相应的方法
网页端
  • 内嵌的HTML文件和uni.webview源文件
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>safeCode</title>
</head>
<style>
  #cf-code {
    height: 65px;
    overflow: hidden;
    background: transparent;
    width: 330px;
  }
  html,
  body {
    background: transparent;
    margin: 0;
    padding: 0;
  }
  iframe {
    border-radius: 10px;
    overflow: hidden;
  }
</style>
<body>
  <div id="cf-code"></div>
  <script src="./uniapp.js"></script>
  <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
  <script type="text/javascript">
    document.addEventListener('UniAppJSBridgeReady', function () {
    // 获取uniapp中url传递过来public_key的参数
      function getUrlParams() {
        const params = new URLSearchParams(window.location.search);
        const result = {};
        for (const [key, value] of params.entries()) {
          result[key] = value;
        }
        return result;
      }
      const params = getUrlParams();
      const cf_params = JSON.stringify(params.cf_params)
      turnstile.ready(function () {
        turnstile.render('#cf-code', {
          sitekey: JSON.parse(cf_params),
          size: "flexible",
          callback: function (response) {
            if (response !== '') {
              // 将cf验证码token信息发送给uniapp
              uni.postMessage({
                data: {
                  action: '1',
                  isShow: true,
                  cf_code: response,
                }
              })
            }
          }
        })
      })
    })
  </script>
</body>
</html>

webview的js文件

// uniapp.js
!function (e, n) { "object" == typeof exports && "undefined" != typeof module ? module.exports = n() : "function" == typeof define && define.amd ? define(n) : (e = e || self).uni = n() }(this, (function () { "use strict"; try { var e = {}; Object.defineProperty(e, "passive", { get: function () { !0 } }), window.addEventListener("test-passive", null, e) } catch (e) { } var n = Object.prototype.hasOwnProperty; function i(e, i) { return n.call(e, i) } var t = []; function r() { return window.__dcloud_weex_postMessage || window.__dcloud_weex_ } var o = function (e, n) { var i = { options: { timestamp: +new Date }, name: e, arg: n }; if (r()) { if ("postMessage" === e) { var o = { data: [n] }; return window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessage(o) : window.__dcloud_weex_.postMessage(JSON.stringify(o)) } var a = { type: "WEB_INVOKE_APPSERVICE", args: { data: i, webviewIds: t } }; window.__dcloud_weex_postMessage ? window.__dcloud_weex_postMessageToService(a) : window.__dcloud_weex_.postMessageToService(JSON.stringify(a)) } if (!window.plus) return window.parent.postMessage({ type: "WEB_INVOKE_APPSERVICE", data: i, pageId: "" }, "*"); if (0 === t.length) { var d = plus.webview.currentWebview(); if (!d) throw new Error("plus.webview.currentWebview() is undefined"); var s = d.parent(), w = ""; w = s ? s.id : d.id, t.push(w) } if (plus.webview.getWebviewById("__uniapp__service")) plus.webview.postMessageToUniNView({ type: "WEB_INVOKE_APPSERVICE", args: { data: i, webviewIds: t } }, "__uniapp__service"); else { var u = JSON.stringify(i); plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat("WEB_INVOKE_APPSERVICE", '",').concat(u, ",").concat(JSON.stringify(t), ");")) } }, a = { navigateTo: function () { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.url; o("navigateTo", { url: encodeURI(n) }) }, navigateBack: function () { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.delta; o("navigateBack", { delta: parseInt(n) || 1 }) }, switchTab: function () { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.url; o("switchTab", { url: encodeURI(n) }) }, reLaunch: function () { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.url; o("reLaunch", { url: encodeURI(n) }) }, redirectTo: function () { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, n = e.url; o("redirectTo", { url: encodeURI(n) }) }, getEnv: function (e) { r() ? e({ nvue: !0 }) : window.plus ? e({ plus: !0 }) : e({ h5: !0 }) }, postMessage: function () { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; o("postMessage", e.data || {}) } }, d = /uni-app/i.test(navigator.userAgent), s = /Html5Plus/i.test(navigator.userAgent), w = /complete|loaded|interactive/; var u = window.my && navigator.userAgent.indexOf(["t", "n", "e", "i", "l", "C", "y", "a", "p", "i", "l", "A"].reverse().join("")) > -1; var g = window.swan && window.swan.webView && /swan/i.test(navigator.userAgent); var v = window.qq && window.qq.miniProgram && /QQ/i.test(navigator.userAgent) && /miniProgram/i.test(navigator.userAgent); var c = window.tt && window.tt.miniProgram && /toutiaomicroapp/i.test(navigator.userAgent); var m = window.wx && window.wx.miniProgram && /micromessenger/i.test(navigator.userAgent) && /miniProgram/i.test(navigator.userAgent); var p = window.qa && /quickapp/i.test(navigator.userAgent); var f = window.ks && window.ks.miniProgram && /micromessenger/i.test(navigator.userAgent) && /miniProgram/i.test(navigator.userAgent); var l = window.tt && window.tt.miniProgram && /Lark|Feishu/i.test(navigator.userAgent); var _ = window.jd && window.jd.miniProgram && /micromessenger/i.test(navigator.userAgent) && /miniProgram/i.test(navigator.userAgent); var E = window.xhs && window.xhs.miniProgram && /xhsminiapp/i.test(navigator.userAgent); for (var h, P = function () { window.UniAppJSBridge = !0, document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady", { bubbles: !0, cancelable: !0 })) }, b = [function (e) { if (d || s) return window.__dcloud_weex_postMessage || window.__dcloud_weex_ ? document.addEventListener("DOMContentLoaded", e) : window.plus && w.test(document.readyState) ? setTimeout(e, 0) : document.addEventListener("plusready", e), a }, function (e) { if (m) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener("WeixinJSBridgeReady", e), window.wx.miniProgram }, function (e) { if (v) return window.QQJSBridge && window.QQJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener("QQJSBridgeReady", e), window.qq.miniProgram }, function (e) { if (u) { document.addEventListener("DOMContentLoaded", e); var n = window.my; return { navigateTo: n.navigateTo, navigateBack: n.navigateBack, switchTab: n.switchTab, reLaunch: n.reLaunch, redirectTo: n.redirectTo, postMessage: n.postMessage, getEnv: n.getEnv } } }, function (e) { if (g) return document.addEventListener("DOMContentLoaded", e), window.swan.webView }, function (e) { if (c) return document.addEventListener("DOMContentLoaded", e), window.tt.miniProgram }, function (e) { if (p) { window.QaJSBridge && window.QaJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener("QaJSBridgeReady", e); var n = window.qa; return { navigateTo: n.navigateTo, navigateBack: n.navigateBack, switchTab: n.switchTab, reLaunch: n.reLaunch, redirectTo: n.redirectTo, postMessage: n.postMessage, getEnv: n.getEnv } } }, function (e) { if (f) return window.WeixinJSBridge && window.WeixinJSBridge.invoke ? setTimeout(e, 0) : document.addEventListener("WeixinJSBridgeReady", e), window.ks.miniProgram }, function (e) { if (l) return document.addEventListener("DOMContentLoaded", e), window.tt.miniProgram }, function (e) { if (_) return window.JDJSBridgeReady && window.JDJSBridgeReady.invoke ? setTimeout(e, 0) : document.addEventListener("JDJSBridgeReady", e), window.jd.miniProgram }, function (e) { if (E) return window.xhs.miniProgram }, function (e) { return document.addEventListener("DOMContentLoaded", e), a }], y = 0; y < b.length && !(h = b[y](P)); y++); h || (h = {}); var B = "undefined" != typeof uni ? uni : {}; if (!B.navigateTo) for (var S in h) i(h, S) && (B[S] = h[S]); return B.webView = h, B }));

通用cf的webview组件

// CodeView.vue
<template>
    <view class="container">
        <web-view ref="webview" :src="webviewSrc" allow :fullscreen="false" :webview-styles="webviewStyles"
            :frameBorder="0" @message="handleMessage"
            style="top:calc(50%-32.5px);background-color: rgba(0, 0, 0, 0) !important;">
        </web-view>
    </view>
</template>
<script>
import config from '../../xxxx.js'
export default {
    name: "CodeView",
    props: {
        cf_params: {
            type: String,
            default: '',
        },
    },
    data() {
        return {
            baseUrl: config.url,
            wv: null,
            webviewStyles: {
                progress: {
                    color: "#3e3adc",
                },
                width: '330px',
                height: "65px",
                margin: "auto",
            },
        };
    },
    computed: {
        webviewSrc() {
            // 当前环境的url不要有localhost否者cf会不通过
            // #ifdef H5
            const url = window.location.origin
            return `${url}${this.baseUrl}/XXXX/index.html?cf_params=${this.cf_params}`;
            // #endif
            // #ifdef APP-PLUS 
            return `${this.baseUrl}/XXXX/index.html?cf_params=${this.cf_params}`;
            // #endif
        }
    },
    methods: {
        // 接收h5页面发来的键值判断需要执行的操作
        handleMessage(event) {
            const code = event.detail.data[0].action
            // 判断 action 值是否为 1
            if (code == 1) {
                setTimeout(() => {
                    this.$emit("update:actionData", event.detail.data[0]);
                }, 200);
            }
            uni.getSystemInfo({
                success: (res) => {
                    this.webviewStyles.height = `${res.screenHeight -
                        res.statusBarHeight -
                        res.safeAreaInsets?.bottom -
                        100
                        }px`;
                    this.webviewStyles.bottom = `${res.safeAreaInsets?.bottom + 56}px`;
                },
            });
            try {
                const data = JSON.parse(event.detail.data);
                switch (data.type) {
                    case "error":
                        this.$emit("error", data.code);
                        break;
                    default:
                        console.warn("未处理的消息类型:", data.type);
                        break;
                }
            } catch (error) {
                console.error("解析消息失败:", error);
            }
        }
    },
};
</script>
<style scoped>
.container {
    background-color: transparent;
}
</style>

页面中使用

<uni-popup ref="turnstile_popup" type="center"  borderRadius="10px" :is-mask-click="false">
  <view class="code_view">
    <CodeView  @update:actionData="handleIsFlag" :cf_params="cf_params" ></CodeView>
  </view>
</uni-popup>
import CodeView from '@/components/CFCodeView/CodeView.vue';
components: {
		CodeView
	},
// 这是后面的逻辑
handleIsFlag(actionData) {
// actionData中包含isShow,cf的token(cf_code), action: '1'
			if (actionData.isShow) {
				this.$refs.turnstile_popup.close();			
				// 走XXX流程
				setTimeout(()=>{
					// 一般会把token传给后端,后端会请求第三方校验公钥和私钥是否匹配
				},200)
			}
		},
// 前面的逻辑
this.$refs.turnstile_popup.open(); // 打开弹窗
this.cf_params = val.public_key; // 传递公钥
需要注意的点
  • uniapp开发时不能直接在页面上面渲染cloudflare中turnstile的内容,会整个铺满页面,弹窗的方案较好点,web开发特别简单,可以直接渲染
  • 环境url,在开发环境中uniapp在HbuilderX中运行是在nodejs环境下,域名是localhost:XXX,这时候cloudflare检验的时候不会通过,这样会很影响开发环境,开发环境后台需要做个开关控制需不需要开启验证

推荐个人项目

  • 这是一个国际化全栈中后台解决方案,支持16种语言/三种主题切换
  • 前端:vue3.5+typescript+vite5+pinia+element-plus+unocss+sass
  • 后端nodejs+express+mysql+redis的管理后台项目
  • 预览:网络需要绕过大陆 kilyicms.vercel.app
  • github代码仓库 kilyicms
  • 创作不易希望点个star⭐⭐⭐
### Cloudflare Turnstile 使用指南和集成方法 #### 什么是Cloudflare TurnstileCloudflare Turnstile 是一种现代的验证码解决方案,旨在保护网站免受自动化攻击的同时提供更好的用户体验。与传统的 reCAPTCHA 不同,Turnstile 提供了更简洁的设计以及更高的隐私保障[^1]。 #### 如何注册并获取站点密钥? 为了使用 Turnstile 验证码服务,开发者需要先访问 [Cloudflare 官方网站](https://www.cloudflare.com/turnstile/) 注册账号,并创建一个新的站点来获得一对公开密钥(sitekey)和私有密钥(secret key)。这两个密钥将在后续配置过程中被广泛用于前端显示验证组件及后端校验请求合法性[^2]。 #### 前端页面集成 要在网页上部署 Turnstile 小部件,需按照如下方式引入 JavaScript 文件: ```html <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> ``` 接着,在 HTML 表单内添加相应的 div 标签以承载该控件实例化的位置: ```html <div class="form-group"> <div id="cf-turnstile"></div> </div> <!-- 初始化 Turnstile --> <script type="text/javascript"> window.onload = function() { var turnstileCallback = function(token) { console.log('Verification successful:', token); }; grecaptcha.ready(function() { grecaptcha.execute('<your-site-key>', {action:'submit'}).then(turnstileCallback); }); }; </script> ``` 请注意替换 `<your-site-key>` 占位符为你自己的 sitekey[^3]。 #### 后端服务器验证 当用户提交表单数据时,会一并将通过隐藏字段传递过来的令牌发送给服务器侧进行进一步确认。此时可以通过 HTTP POST 请求向 `https://challenges.cloudflare.com/turnstile/siteverify` 发送包含 secret_key 和 response 参数的消息体来进行真实性检验。成功返回 JSON 对象中 score 字段表示此次交互行为可信度得分;若低于设定阈值则视为可疑操作处理[^4]。 ```python import requests def verify_turnstile_token(secret, response): url = "https://challenges.cloudflare.com/turnstile/siteverify" payload = {'secret': secret, 'response': response} r = requests.post(url, data=payload) result = r.json() return result['success'], result.get('score') ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值