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

14 KiB
Raw Blame History

阿里云滑块验证码接入说明(含加密模式)

  • 前端需要做什么
  • 后端需要做什么
  • 非加密模式 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 SDKcaptcha-20230305+ 我们的封装,对 captchaVerifyParam 做服务端校验:
    • 校验通过:继续后续业务逻辑(发短信、查询等)。
    • 校验失败:直接返回业务错误,例如「图形验证码校验失败」。
  4. 对于 加密模式 场景,前端还需要在初始化时传入 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 // 加密模式用的 ekeyBase64
}
  • 文件:app/main/api/etc/main.yaml / main.dev.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
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
@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.goCaptchaConfig 增加 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.goGenerateEncryptedSceneID 实现。
    • pkg/captcha/aliyun.goVerify 实现(调用阿里云 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. 时间同步

    • 加密模式依赖 timestampexpireTime,服务器时间要尽量准确(建议 NTP 同步)。
  3. 前后端模式一致

    • 如果控制台开启了加密模式,前端必须带上 EncryptedSceneId
    • 如果前端只传 SceneId,在加密模式下会被阿里云直接拒绝(出现类似 F022 错误码)。
  4. 错误处理

    • 服务端 Verify 出错(网络、阿里云故障)时,我们当前策略是记录日志但视为通过,防止影响业务可用性——这一点可根据各平台风险偏好调整。
  5. 复用策略

    • 不同前端技术栈Vue/React/小程序等),只要能做到:
      1. 初始化时根据配置决定是否从后端拿 EncryptedSceneId
      2. 在业务请求前通过验证码拿到 captchaVerifyParam 并传给后端;
    • 后端则统一在与风控相关的接口上调用相同的 Verify 封装即可。

六、推荐的接入步骤

  1. 在阿里云验证码控制台创建场景,记下 SceneIdekey,并根据需要打开「加密模式」。
  2. 在你自己的后端项目里:
    • 增加 Captcha 配置块AccessKeyID、AccessKeySecret、EndpointURL、SceneID、EKey
    • 实现 EncryptedSceneId 生成接口 /captcha/encryptedSceneId(可参考本项目的 GenerateEncryptedSceneID
    • 在需要滑块的业务接口(发短信、查询)前调用阿里云 SDK 做 captchaVerifyParam 校验。
  3. 在你自己的前端项目里:
    • 页面引入 AliyunCaptcha.js 并预留验证码容器;
    • 抽一个类似 runWithCaptcha(bizVerify, onSuccess) 的封装;
    • 业务按钮点击时,不直接调接口,而是先触发 runWithCaptcha
      • 前端拉取 EncryptedSceneId(若启用加密模式);
      • 初始化 initAliyunCaptcha 并弹出滑块;
      • 滑块通过后才真正调用后端业务接口。