## 阿里云滑块验证码接入说明(含加密模式) - 前端需要做什么 - 后端需要做什么 - 非加密模式 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 ## 一、整体流程概览 ### 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 ...
``` 注意: - `#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 // 加密模式用的 ekey(Base64) } ``` - 文件:`app/main/api/etc/main.yaml` / `main.dev.yaml` ```yaml Captcha: AccessKeyID: "你的AccessKeyId" AccessKeySecret: "你的AccessKeySecret" EndpointURL: "captcha.cn-shanghai.aliyuncs.com" SceneID: "控制台场景ID" EKey: "控制台上看到的 ekey(Base64 字符串)" ``` > 其它平台(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×tamp&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` 并弹出滑块; - 滑块通过后才真正调用后端业务接口。