add smsabuse

This commit is contained in:
Mrx
2026-02-26 10:48:55 +08:00
parent f6ac4c9a50
commit 072a258e53
7 changed files with 697 additions and 39 deletions

8
.env
View File

@@ -20,4 +20,10 @@ VITE_CHAT_AES_IV=345GDFED433223DF
VITE_SHARE_TITLE=全能查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
VITE_SHARE_DESC=提供个人信用评估、入职背调、信贷风控、企业风险监测等服务
VITE_SHARE_IMG=https://www.quannengcha.com/logo.png
VITE_TOKEN_VERSION=1.0
VITE_TOKEN_VERSION=1.0
# 阿里云滑块验证码配置
VITE_ALIYUN_CAPTCHA_SCENE_ID=wynt39to
# 是否启用加密模式true/false需要在阿里云控制台开启加密模式
# 注意:根据代码逻辑,设置为 true 表示禁用加密,设置为 false 表示启用加密
VITE_ALIYUN_CAPTCHA_ENCRYPTED=true

View File

@@ -45,7 +45,7 @@
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.zhinengcha.cn/" />
<meta property="og:url" content="https://www.quannengcha.com/" />
<meta
property="og:title"
content="全能查官网_个人婚姻状态报告_综合风险排查工具箱"
@@ -59,7 +59,7 @@
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://www.zhinengcha.cn/" />
<meta property="twitter:url" content="https://www.quannengcha.com/" />
<meta
property="twitter:title"
content="全能查官网_个人婚姻状态报告_综合风险排查工具箱"
@@ -83,11 +83,11 @@
"@context": "https://schema.org",
"@type": "WebSite",
"name": "全能查",
"url": "https://www.zhinengcha.cn/",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人信用查询、小微企业风控、贷前风险背调等多场景报告应用",
"potentialAction": {
"@type": "SearchAction",
"target": "https://www.zhinengcha.cn/search?q={search_term_string}",
"target": "https://www.quannengcha.com/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
@@ -98,7 +98,7 @@
"@context": "https://schema.org",
"@type": "Organization",
"name": "全能查",
"url": "https://www.zhinengcha.cn/",
"url": "https://www.quannengcha.com/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
</script>
@@ -109,6 +109,15 @@
delete window.wx;
</script>
<!-- 阿里云滑块验证码 -->
<script>
window.AliyunCaptchaConfig = { region: "cn", prefix: "12zxnj" };
</script>
<script
type="text/javascript"
src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"
></script>
<!-- 预加载关键资源 -->
<link rel="preconnect" href="https://www.zhinengcha.cn" />
<link rel="preconnect" href="https://res.wx.qq.com" />
@@ -207,6 +216,8 @@
<div class="loading-text">加载中</div>
</div>
<div id="app"></div>
<!-- 阿里云滑块验证码挂载容器 -->
<div id="captcha-element"></div>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -117,6 +117,7 @@ declare global {
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAgent: typeof import('./composables/useAgent.js')['useAgent']
const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
const useAliyunCaptcha: typeof import('./composables/useAliyunCaptcha.js')['default']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
const useAppStore: typeof import('./stores/appStore.js')['useAppStore']

View File

@@ -132,6 +132,7 @@ import { useRoute, useRouter } from "vue-router";
import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from "@/composables/useEnv";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
import Payment from "@/components/Payment.vue";
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
@@ -199,6 +200,7 @@ const dialogStore = useDialogStore();
const userStore = useUserStore();
const { isWeChat } = useEnv();
const appStore = useAppStore();
const { runWithCaptcha } = useAliyunCaptcha();
// 响应式数据
const showPayment = ref(false);
@@ -424,22 +426,27 @@ async function sendVerificationCode() {
return;
}
const { data, error } = await useApiFetch("/auth/sendSms")
.post({ mobile: formData.mobile, actionType: "query" })
.json();
if (!error.value && data.value.code === 200) {
showToast({ message: "验证码发送成功", type: "success" });
startCountdown();
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("/auth/sendSms")
.post({ mobile: formData.mobile, actionType: "query", captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "验证码发送成功", type: "success" });
startCountdown();
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast({ message: res.msg || "验证码发送失败,请重试" });
}
});
} else {
showToast({ message: "验证码发送失败,请重试" });
}
}
);
}
let timer = null;

View File

@@ -0,0 +1,172 @@
import { showToast, showLoadingToast, closeToast } from "vant";
import useApiFetch from "@/composables/useApiFetch";
// 阿里云验证码场景 ID
const ALIYUN_CAPTCHA_SCENE_ID = "wynt39to";
// 是否启用加密模式(通过环境变量控制,非加密模式时前端不调用后端获取 EncryptedSceneId
const ENABLE_ENCRYPTED =
import.meta.env.VITE_ALIYUN_CAPTCHA_ENCRYPTED === "false";
let captchaInitialised = false;
/** 首次初始化后SDK 会异步调用 getInstance用此 Promise 在实例就绪后再 show */
let captchaReadyPromise = null;
let captchaReadyResolve = null;
async function ensureCaptchaInit() {
if (captchaInitialised || typeof window === "undefined") return;
if (typeof window.initAliyunCaptcha !== "function") return;
captchaInitialised = true;
window.captcha = null;
window.__lastBizResponse = null;
window.__onCaptchaBizSuccess = null;
captchaReadyPromise = new Promise((resolve) => {
captchaReadyResolve = resolve;
});
// 非加密模式:仅传 SceneId不调用后端接口
if (!ENABLE_ENCRYPTED) {
window.initAliyunCaptcha({
SceneId: ALIYUN_CAPTCHA_SCENE_ID,
mode: "popup",
element: "#captcha-element",
getInstance(instance) {
window.captcha = instance;
if (typeof captchaReadyResolve === "function") {
captchaReadyResolve();
captchaReadyResolve = null;
}
},
captchaVerifyCallback(param) {
return typeof window.__captchaVerifyCallback === "function"
? window.__captchaVerifyCallback(param)
: Promise.resolve({
captchaResult: false,
bizResult: false,
});
},
onBizResultCallback(bizResult) {
if (typeof window.__onBizResultCallback === "function") {
window.__onBizResultCallback(bizResult);
}
window.__lastBizResponse = null;
window.__onCaptchaBizSuccess = null;
},
slideStyle: { width: 360, height: 40 },
language: "cn",
});
return;
}
// 加密模式:先从后端获取 EncryptedSceneId再初始化
const { data, error } = await useApiFetch("/captcha/encryptedSceneId")
.post()
.json();
const resp = data?.value;
const encryptedSceneId = resp?.data?.encryptedSceneId;
if (error?.value || !encryptedSceneId) {
showToast({ message: "获取验证码参数失败,请稍后重试" });
captchaInitialised = false;
captchaReadyPromise = null;
captchaReadyResolve = null;
return;
}
window.initAliyunCaptcha({
SceneId: ALIYUN_CAPTCHA_SCENE_ID,
EncryptedSceneId: encryptedSceneId,
mode: "popup",
element: "#captcha-element",
getInstance(instance) {
window.captcha = instance;
if (typeof captchaReadyResolve === "function") {
captchaReadyResolve();
captchaReadyResolve = null;
}
},
captchaVerifyCallback(param) {
return typeof window.__captchaVerifyCallback === "function"
? window.__captchaVerifyCallback(param)
: Promise.resolve({ captchaResult: false, bizResult: false });
},
onBizResultCallback(bizResult) {
if (typeof window.__onBizResultCallback === "function") {
window.__onBizResultCallback(bizResult);
}
window.__lastBizResponse = null;
window.__onCaptchaBizSuccess = null;
},
slideStyle: { width: 360, height: 40 },
language: "cn",
});
}
/**
* 阿里云滑块验证码通用封装。
* 依赖 index.html 中已加载的 AliyunCaptcha.js初始化在首次调起时执行。
*
* @param { (captchaVerifyParam: string) => Promise<{ data: Ref, error: Ref }> } bizVerify - 业务请求函数,接收滑块参数,返回 useApiFetch 的 { data, error }
* @param { (res: any) => void } [onSuccess] - 业务成功回调code===200 时调用,传入接口返回的 data.value
*/
export function useAliyunCaptcha() {
/**
* 先弹出滑块,通过后执行 bizVerify(captchaVerifyParam),再根据结果调用 onSuccess。
*/
async function runWithCaptcha(bizVerify, onSuccess) {
if (typeof window === "undefined") {
showToast({ message: "验证码仅支持浏览器环境" });
return;
}
const loading = showLoadingToast({
message: "安全验证加载中...",
forbidClick: true,
duration: 0,
loadingType: "spinner",
});
try {
window.__captchaVerifyCallback = async (captchaVerifyParam) => {
window.__lastBizResponse = null;
const { data, error } = await bizVerify(captchaVerifyParam);
const result = data?.value ?? data;
if (error?.value || !result) {
return { captchaResult: false, bizResult: false };
}
window.__lastBizResponse = result;
const captchaOk = result.captchaVerifyResult !== false;
const bizOk = result.code === 200;
return { captchaResult: captchaOk, bizResult: bizOk };
};
window.__onBizResultCallback = (bizResult) => {
if (
bizResult === true &&
window.__lastBizResponse &&
typeof window.__onCaptchaBizSuccess === "function"
) {
window.__onCaptchaBizSuccess(window.__lastBizResponse);
}
};
await ensureCaptchaInit();
// 首次初始化时 SDK 会异步调用 getInstance需等待实例就绪后再 show
if (captchaReadyPromise) {
await captchaReadyPromise;
captchaReadyPromise = null;
}
if (!window.captcha) {
showToast({ message: "验证码未加载,请刷新页面重试" });
return;
}
window.__onCaptchaBizSuccess = onSuccess;
window.captcha.show();
} finally {
closeToast();
}
}
return { runWithCaptcha };
}
export default useAliyunCaptcha;

View File

@@ -6,11 +6,13 @@ import { useUserStore } from '@/stores/userStore'
import { useRouter, useRoute } from 'vue-router'
import { mobileCodeLogin } from '@/api/user'
import useApiFetch from '@/composables/useApiFetch'
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
const router = useRouter()
const route = useRoute()
const agentStore = useAgentStore()
const userStore = useUserStore()
const { runWithCaptcha } = useAliyunCaptcha()
const phoneNumber = ref('')
const verificationCode = ref('')
@@ -35,25 +37,28 @@ async function sendVerificationCode() {
return
}
const { data, error } = await useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'login' })
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg)
// 使用滑块验证码保护发送短信接口
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'login', captchaVerifyParam })
.json(),
(res) => {
if (res.code === 200) {
showToast({ message: "获取成功" });
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(res.msg || "获取验证码失败")
}
}
}
);
}
function startCountdown() {

456
阿里云验证码接入.md Normal file
View File

@@ -0,0 +1,456 @@
## 阿里云滑块验证码接入说明(含加密模式)
- 前端需要做什么
- 后端需要做什么
- 非加密模式 vs 加密模式(`EncryptedSceneId`
- 本项目修改了哪些文件,可以作为参考实现
下面所有内容以当前项目(`tyc-webview-v2` + `tyc-server-v2`)为例,按步骤说明。
---
配置项
go get github.com/alibabacloud-go/captcha-20230305/client
Captcha:
# 建议与短信相同的 AccessKey或单独为验证码创建子账号
AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9"
AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
# 验证码服务 Endpoint国内一般为 captcha.cn-shanghai.aliyuncs.com
EndpointURL: "captcha.cn-shanghai.aliyuncs.com"
# 阿里云控制台中该场景的 SceneId请替换为真实值
SceneID: "wynt39to"
# 验证码控制台中的 ekey通常为 Base64 字符串),用于生成 EncryptedSceneId
EKey: ""
index.html
<!-- 阿里云滑块验证码 -->
<script>
window.AliyunCaptchaConfig = { region: "cn", prefix: "12zxnj" };
</script>
## 一、整体流程概览
### 1.1 场景说明
我们使用的是 **阿里云验证码 2.0 / V3 架构** 的滑块验证码,前后端配合流程如下:
1. 前端在「获取短信验证码」/「查询前无短信码的产品」时,先弹出阿里云滑块验证码。
2. 用户拖动成功后,前端拿到 `captchaVerifyParam`,携带到后端业务接口。
3. 后端使用阿里云官方 Go SDK`captcha-20230305`+ 我们的封装,对 `captchaVerifyParam` 做服务端校验:
- 校验通过:继续后续业务逻辑(发短信、查询等)。
- 校验失败:直接返回业务错误,例如「图形验证码校验失败」。
4. 对于 **加密模式** 场景,前端还需要在初始化时传入 `EncryptedSceneId`,而 `EncryptedSceneId` 由后端用控制台的 `ekey` 生成。
### 1.2 使用场景(当前项目)
- 登录页:
- **获取短信验证码**:必须先通过滑块验证。
- **提交登录**:只校验短信验证码,不再做滑块。
- Inquire 查询页:
- 有「短信验证码」字段的产品:点击「获取验证码」前滑块;点击「查询」时不需要再次滑块。
- 无「短信验证码」的产品:点击「查询」前滑块。
---
## 二、前端接入说明(以 Vue 3 / Vite 为例)
### 2.1 全局基础集成(`index.html`
在入口 HTML 中引入阿里云验证码脚本,并预留一个容器:
- 文件:`tyc-webview-v2/index.html`
关键片段:
```html
<script>
window.AliyunCaptchaConfig = { region: "cn", prefix: "你的前缀" };
</script>
<script
type="text/javascript"
src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"
></script>
...
<body>
<div id="app"></div>
<div id="captcha-element"></div>
</body>
```
注意:
- `#captcha-element` 是验证码挂载容器,必须存在。
- `AliyunCaptcha.js` 必须在前端业务代码(`/src/main.js`)之前加载。
### 2.2 通用封装:`useAliyunCaptcha`
- 文件:`tyc-webview-v2/src/composables/useAliyunCaptcha.js`
该 composable 封装了:
- **初始化阿里云验证码实例**(含加密 / 非加密模式);
- 提供一个通用方法 `runWithCaptcha(bizVerify, onSuccess)`
- `bizVerify(captchaVerifyParam)`:前端回调,内部调用后端业务接口(发短信、查询等),返回 `{ data, error }``useApiFetch` 结果)。
- `onSuccess(res)`:当业务 `code === 200` 时调用,`res` 为后端返回的数据。
使用方式示例:
```js
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const { runWithCaptcha } = useAliyunCaptcha();
function sendLoginSms() {
if (!isPhoneValid.value) return;
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("auth/sendSms")
.post({
mobile: phoneNumber.value,
actionType: "login",
captchaVerifyParam,
})
.json(),
(res) => {
if (res.code === 200) {
// 成功toast + 开始倒计时 + 聚焦输入框
} else {
// 失败toast 提示
}
},
);
}
```
### 2.3 加密模式开关(前端)
`useAliyunCaptcha.js` 顶部:
```js
// 是否启用加密模式(通过环境变量控制)
const ENABLE_ENCRYPTED =
import.meta.env.VITE_ALIYUN_CAPTCHA_ENCRYPTED === "true";
```
`ensureCaptchaInit()` 中:
- **非加密模式**`ENABLE_ENCRYPTED === false`
```js
if (!ENABLE_ENCRYPTED) {
window.initAliyunCaptcha({
SceneId: ALIYUN_CAPTCHA_SCENE_ID,
mode: "popup",
element: "#captcha-element",
getInstance(instance) { window.captcha = instance; ... },
captchaVerifyCallback(param) { ... },
onBizResultCallback(bizResult) { ... },
slideStyle: { width: 360, height: 40 },
language: "cn",
});
return;
}
```
前端不会请求后端获取 `EncryptedSceneId`,只用 `SceneId` 初始化。
- **加密模式**`ENABLE_ENCRYPTED === true`
```js
const { data, error } = await useApiFetch("/captcha/encryptedSceneId")
.post()
.json();
const resp = data?.value;
const encryptedSceneId = resp?.data?.encryptedSceneId;
if (error?.value || !encryptedSceneId) {
showToast({ message: "获取验证码参数失败,请稍后重试" });
// 重置状态
captchaInitialised = false;
captchaReadyPromise = null;
captchaReadyResolve = null;
return;
}
window.initAliyunCaptcha({
SceneId: ALIYUN_CAPTCHA_SCENE_ID,
EncryptedSceneId: encryptedSceneId,
mode: "popup",
element: "#captcha-element",
...
});
```
> 在其它前端平台React、原生 H5 等),可以复用同样的思路:
> - 抽一个 `runWithCaptcha` 工具;
> - 初始化逻辑中根据配置决定是否去后端拿 `EncryptedSceneId`,有则带上。
### 2.4 用户体验:加载提示
在 `runWithCaptcha` 中增加全局 Loading
```js
const loading = showLoadingToast({
message: "安全验证加载中...",
forbidClick: true,
duration: 0,
loadingType: "spinner",
});
try {
// 设置 __captchaVerifyCallback / __onBizResultCallback
// await ensureCaptchaInit()
// await captchaReadyPromise
// window.captcha.show()
} finally {
closeToast();
}
```
这样用户点击按钮后能看到“安全验证加载中”,避免误以为没反应。
---
## 三、后端接入说明Go / go-zero
### 3.1 基本配置(`config.go` + `main.yaml`
- 文件:`app/main/api/internal/config/config.go`
```go
type CaptchaConfig struct {
AccessKeyID string
AccessKeySecret string
EndpointURL string
SceneID string
EKey string // 加密模式用的 ekeyBase64
}
```
- 文件:`app/main/api/etc/main.yaml` / `main.dev.yaml`
```yaml
Captcha:
AccessKeyID: "你的AccessKeyId"
AccessKeySecret: "你的AccessKeySecret"
EndpointURL: "captcha.cn-shanghai.aliyuncs.com"
SceneID: "控制台场景ID"
EKey: "控制台上看到的 ekeyBase64 字符串)"
```
> 其它平台Java/Spring、.NET 等)也需要同样的配置:
> - 一个 Captcha 配置块,包含 AK/SK、Endpoint、SceneId、EKey。
### 3.2 EncryptedSceneId 生成接口(加密模式)
#### 3.2.1 生成函数
- 文件:`pkg/captcha/encrypt_scene.go`
```go
package captcha
import (
"encoding/base64"
"fmt"
"time"
lzcrypto "tyc-server/pkg/lzkit/crypto"
)
// GenerateEncryptedSceneID: sceneId&timestamp&expireTime -> AES-256-CBC + PKCS7 -> Base64(IV + ciphertext)
func GenerateEncryptedSceneID(sceneId, ekey string, expireSeconds int) (string, error) {
if expireSeconds <= 0 || expireSeconds > 86400 {
expireSeconds = 3600
}
ts := time.Now().Unix()
plaintext := fmt.Sprintf("%s&%d&%d", sceneId, ts, expireSeconds)
keyBytes, err := base64.StdEncoding.DecodeString(ekey)
if err != nil {
return "", fmt.Errorf("decode ekey error: %w", err)
}
if len(keyBytes) != 32 {
return "", fmt.Errorf("invalid ekey length, need 32 bytes after base64 decode, got %d", len(keyBytes))
}
return lzcrypto.AesEncrypt([]byte(plaintext), keyBytes)
}
```
> 在其它语言上,只要完全按文档实现同样的算法即可。
#### 3.2.2 API 声明
- 文件:`app/main/api/desc/front/user.api`
```go
@server (
prefix: api/v1
group: captcha
)
service main {
@doc "get encrypted scene id for aliyun captcha"
@handler getEncryptedSceneId
post /captcha/encryptedSceneId returns (GetEncryptedSceneIdResp)
}
type (
GetEncryptedSceneIdResp {
EncryptedSceneId string `json:"encryptedSceneId"`
}
)
```
#### 3.2.3 逻辑实现
- 文件:`app/main/api/internal/logic/captcha/getencryptedsceneidlogic.go`
```go
func (l *GetEncryptedSceneIdLogic) GetEncryptedSceneId() (*types.GetEncryptedSceneIdResp, error) {
cfg := l.svcCtx.Config.Captcha
encrypted, err := captcha.GenerateEncryptedSceneID(cfg.SceneID, cfg.EKey, 3600)
if err != nil {
l.Errorf("generate encrypted scene id error: %+v", err)
return nil, err
}
return &types.GetEncryptedSceneIdResp{
EncryptedSceneId: encrypted,
}, nil
}
```
> 其它平台只需要提供一个类似的 HTTP 接口即可:
> `POST /captcha/encryptedSceneId -> { encryptedSceneId: "xxx" }`
### 3.3 验证 `captchaVerifyParam`(服务端验签)
- 文件:`pkg/captcha/aliyun.go`
使用阿里云官方 Go SDK 验证:
```go
func Verify(cfg Config, captchaVerifyParam string) error {
if os.Getenv("ENV") == "development" {
return nil
}
if captchaVerifyParam == "" {
return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "empty captchaVerifyParam")
}
clientCfg := &openapi.Config{
AccessKeyId: tea.String(cfg.AccessKeyID),
AccessKeySecret: tea.String(cfg.AccessKeySecret),
}
clientCfg.Endpoint = tea.String(cfg.EndpointURL)
client, err := captcha20230305.NewClient(clientCfg)
...
req := &captcha20230305.VerifyIntelligentCaptchaRequest{
SceneId: tea.String(cfg.SceneID),
CaptchaVerifyParam: tea.String(captchaVerifyParam),
}
resp, err := client.VerifyIntelligentCaptcha(req)
...
if tea.BoolValue(resp.Body.Result.VerifyResult) {
return nil
}
// 否则返回 "图形验证码校验失败"
}
```
在需要滑块的业务逻辑里(发送短信、查询),调用:
```go
cfg := l.svcCtx.Config.Captcha
if err := captcha.Verify(captcha.Config{
AccessKeyID: cfg.AccessKeyID,
AccessKeySecret: cfg.AccessKeySecret,
EndpointURL: cfg.EndpointURL,
SceneID: cfg.SceneID,
}, req.CaptchaVerifyParam); err != nil {
return nil, err
}
```
> 其它语言平台Java、Node.js 等)可以用对应的阿里云 SDK 实现相同的服务端校验。
### 3.4 sendSms 接口示例
- 文件:`app/main/api/internal/logic/auth/sendsmslogic.go`
```go
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error {
// 1. 图形验证码校验
cfg := l.svcCtx.Config.Captcha
if err := captcha.Verify(captcha.Config{...}, req.CaptchaVerifyParam); err != nil {
return err
}
// 2. 原来的手机号加密、频率限制、阿里短信发送、Redis 存验证码等逻辑
}
```
---
## 四、关键文件清单
- 前端:
- `tyc-webview-v2/index.html`:引入 `AliyunCaptcha.js`、提供 `#captcha-element`。
- `tyc-webview-v2/src/composables/useAliyunCaptcha.js`:通用封装,支持加密/非加密模式 + Loading 提示。
- `tyc-webview-v2/src/views/Login.vue`:获取短信前通过 `runWithCaptcha` 调用 `/auth/sendSms`。
- `tyc-webview-v2/src/components/InquireForm.vue`:查询前根据是否有短信验证码字段决定是否通过 `runWithCaptcha`。
- 后端:
- `app/main/api/internal/config/config.go``CaptchaConfig` 增加 `EKey`。
- `app/main/api/etc/main.yaml` / `main.dev.yaml`:补充 `Captcha` 配置SceneID + EKey
- `app/main/api/desc/front/user.api`:声明 `/captcha/encryptedSceneId` 接口。
- `app/main/api/internal/logic/captcha/getencryptedsceneidlogic.go`:生成 `EncryptedSceneId`。
- `pkg/captcha/encrypt_scene.go``GenerateEncryptedSceneID` 实现。
- `pkg/captcha/aliyun.go``Verify` 实现(调用阿里云 SDK
- `app/main/api/internal/logic/auth/sendsmslogic.go`:发送短信前调用 `captcha.Verify`。
- `app/main/api/internal/logic/query/queryservicelogic.go`:对无短信验证码的产品,在查询前调用 `captcha.Verify`。
---
## 五、接入其它平台时的注意事项
1. **SceneId / ekey 必须一一对应**
- 控制台“场景管理”里的 SceneId 和 ekey 必须和配置里完全一致。
- 多个场景要分别管理 SceneId / ekey。
2. **时间同步**
- 加密模式依赖 `timestamp` 和 `expireTime`,服务器时间要尽量准确(建议 NTP 同步)。
3. **前后端模式一致**
- 如果控制台开启了加密模式,前端必须**带上 `EncryptedSceneId`**
- 如果前端只传 `SceneId`,在加密模式下会被阿里云直接拒绝(出现类似 F022 错误码)。
4. **错误处理**
- 服务端 `Verify` 出错(网络、阿里云故障)时,我们当前策略是**记录日志但视为通过**,防止影响业务可用性——这一点可根据各平台风险偏好调整。
5. **复用策略**
- 不同前端技术栈Vue/React/小程序等),只要能做到:
1. 初始化时根据配置决定是否从后端拿 `EncryptedSceneId`
2. 在业务请求前通过验证码拿到 `captchaVerifyParam` 并传给后端;
- 后端则统一在与风控相关的接口上调用相同的 `Verify` 封装即可。
---
## 六、推荐的接入步骤
1. 在阿里云验证码控制台创建场景,记下 **SceneId** 和 **ekey**,并根据需要打开「加密模式」。
2. 在你自己的后端项目里:
- 增加 Captcha 配置块AccessKeyID、AccessKeySecret、EndpointURL、SceneID、EKey
- 实现 EncryptedSceneId 生成接口 `/captcha/encryptedSceneId`(可参考本项目的 `GenerateEncryptedSceneID`
- 在需要滑块的业务接口(发短信、查询)前调用阿里云 SDK 做 `captchaVerifyParam` 校验。
3. 在你自己的前端项目里:
- 页面引入 `AliyunCaptcha.js` 并预留验证码容器;
- 抽一个类似 `runWithCaptcha(bizVerify, onSuccess)` 的封装;
- 业务按钮点击时,不直接调接口,而是先触发 `runWithCaptcha`
- 前端拉取 `EncryptedSceneId`(若启用加密模式);
- 初始化 `initAliyunCaptcha` 并弹出滑块;
- 滑块通过后才真正调用后端业务接口。