Files
qnc-webview-v3/阿里云验证码接入.md
2026-02-26 10:48:55 +08:00

457 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 阿里云滑块验证码接入说明(含加密模式)
- 前端需要做什么
- 后端需要做什么
- 非加密模式 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` 并弹出滑块;
- 滑块通过后才真正调用后端业务接口。