14 KiB
阿里云滑块验证码接入说明(含加密模式)
- 前端需要做什么
- 后端需要做什么
- 非加密模式 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 架构 的滑块验证码,前后端配合流程如下:
- 前端在「获取短信验证码」/「查询前无短信码的产品」时,先弹出阿里云滑块验证码。
- 用户拖动成功后,前端拿到
captchaVerifyParam,携带到后端业务接口。 - 后端使用阿里云官方 Go SDK(
captcha-20230305)+ 我们的封装,对captchaVerifyParam做服务端校验:- 校验通过:继续后续业务逻辑(发短信、查询等)。
- 校验失败:直接返回业务错误,例如「图形验证码校验失败」。
- 对于 加密模式 场景,前端还需要在初始化时传入
EncryptedSceneId,而EncryptedSceneId由后端用控制台的ekey生成。
1.2 使用场景(当前项目)
- 登录页:
- 获取短信验证码:必须先通过滑块验证。
- 提交登录:只校验短信验证码,不再做滑块。
- Inquire 查询页:
- 有「短信验证码」字段的产品:点击「获取验证码」前滑块;点击「查询」时不需要再次滑块。
- 无「短信验证码」的产品:点击「查询」前滑块。
二、前端接入说明(以 Vue 3 / Vite 为例)
2.1 全局基础集成(index.html)
在入口 HTML 中引入阿里云验证码脚本,并预留一个容器:
- 文件:
tyc-webview-v2/index.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为后端返回的数据。
使用方式示例:
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 顶部:
// 是否启用加密模式(通过环境变量控制)
const ENABLE_ENCRYPTED =
import.meta.env.VITE_ALIYUN_CAPTCHA_ENCRYPTED === "true";
在 ensureCaptchaInit() 中:
-
非加密模式(
ENABLE_ENCRYPTED === false):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):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:
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
type CaptchaConfig struct {
AccessKeyID string
AccessKeySecret string
EndpointURL string
SceneID string
EKey string // 加密模式用的 ekey(Base64)
}
- 文件:
app/main/api/etc/main.yaml/main.dev.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
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
@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
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 验证:
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
}
// 否则返回 "图形验证码校验失败"
}
在需要滑块的业务逻辑里(发送短信、查询),调用:
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
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。
五、接入其它平台时的注意事项
-
SceneId / ekey 必须一一对应
- 控制台“场景管理”里的 SceneId 和 ekey 必须和配置里完全一致。
- 多个场景要分别管理 SceneId / ekey。
-
时间同步
- 加密模式依赖
timestamp和expireTime,服务器时间要尽量准确(建议 NTP 同步)。
- 加密模式依赖
-
前后端模式一致
- 如果控制台开启了加密模式,前端必须带上
EncryptedSceneId; - 如果前端只传
SceneId,在加密模式下会被阿里云直接拒绝(出现类似 F022 错误码)。
- 如果控制台开启了加密模式,前端必须带上
-
错误处理
- 服务端
Verify出错(网络、阿里云故障)时,我们当前策略是记录日志但视为通过,防止影响业务可用性——这一点可根据各平台风险偏好调整。
- 服务端
-
复用策略
- 不同前端技术栈(Vue/React/小程序等),只要能做到:
- 初始化时根据配置决定是否从后端拿
EncryptedSceneId; - 在业务请求前通过验证码拿到
captchaVerifyParam并传给后端;
- 初始化时根据配置决定是否从后端拿
- 后端则统一在与风控相关的接口上调用相同的
Verify封装即可。
- 不同前端技术栈(Vue/React/小程序等),只要能做到:
六、推荐的接入步骤
- 在阿里云验证码控制台创建场景,记下 SceneId 和 ekey,并根据需要打开「加密模式」。
- 在你自己的后端项目里:
- 增加 Captcha 配置块(AccessKeyID、AccessKeySecret、EndpointURL、SceneID、EKey);
- 实现 EncryptedSceneId 生成接口
/captcha/encryptedSceneId(可参考本项目的GenerateEncryptedSceneID); - 在需要滑块的业务接口(发短信、查询)前调用阿里云 SDK 做
captchaVerifyParam校验。
- 在你自己的前端项目里:
- 页面引入
AliyunCaptcha.js并预留验证码容器; - 抽一个类似
runWithCaptcha(bizVerify, onSuccess)的封装; - 业务按钮点击时,不直接调接口,而是先触发
runWithCaptcha:- 前端拉取
EncryptedSceneId(若启用加密模式); - 初始化
initAliyunCaptcha并弹出滑块; - 滑块通过后才真正调用后端业务接口。
- 前端拉取
- 页面引入