加密模式接入引导

更新时间:

加密模式是一项可选的安全增强功能。启用后,您的服务端需使用密钥(ekey)动态生成加密字符串,并传递给前端用于验证码初始化。该模式可有效防止因 sceneId 泄露而导致的服务盗用或恶意调用,从而提升接口安全性。

工作原理

  1. 服务端生成加密字符串:业务服务端使用从验证码控制台获取的密钥(ekey),将场景ID(sceneId)、时间戳和有效期等信息加密,生成一个有时效性的加密字符串(EncryptedSceneId)。

  2. 前端初始化验证码:前端从业务服务端获取该加密字符串,并在初始化验证码时将其作为参数传递。

  3. 验证码服务校验:验证码服务端接收到请求后,将使用在控制台开启的加密模式进行强制解密和安全校验。校验失败的请求将被拒绝。

步骤一:服务端生成加密字符串

获取密钥(ekey)

登录阿里云验证码控制台,在概览页面右上方区域,获取用于加密的密钥(ekey)

image.png

构造待加密数据并执行加密

  1. 构造明文数据字符串。

    • 数据格式:

      sceneId&timestamp&expireTime
    • 参数说明:

      • sceneId (String):验证码场景的唯一标识(原 CaptchaAppid)。

      • timestamp (Long):当前时间的 Unix 时间戳(单位:秒)。

        说明

        此时间戳不得晚于服务器接收请求的时间。

      • expireTime (Int):加密字符串的有效时长(单位:秒),取值范围为 1 至 86400(即 1秒 至 24小时)。

    • 数据示例:

      sdewfwe&1712345678&3600
  2. 执行 AES-256 加密。

    使用以下参数对上一步的明文字符串进行 AES 加密,得到加密后的字节数组( CaptchaSceneIdEncrypted)。

    • 加密算法:AES

    • 模式:CBC

    • 填充方式:PKCS7Padding

    • 密钥(Key):使用前面步骤获取的密钥(ekey)

    • 初始化向量(IV):一个随机生成的 16 字节数组。

      重要

      为确保安全性,每次加密操作都必须使用一个全新的、通过密码学安全随机数生成器产生的 IV。严禁复用 IV。

  3. 拼接并进行 Base64 编码。

    1. 将 16 字节的 IV 与 AES 加密后的密文字节数组按顺序拼接。拼接格式:IV + CaptchaSceneIdEncrypted,中间无连接符。

    2. 对拼接后的字节数组进行 Base64 编码(标准编码,不含换行符)。

    3. 最终得到的字符串即为传递给前端的 EncryptedSceneId

服务端代码示例 (Java)

以下示例展示了如何生成 EncryptedSceneId

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Base64;

public class SceneIdEncryptor {

    // IV 固定为 16 字节 (AES block size)
    private static final int IV_LENGTH_BYTES = 16;

    /**
     * 对 sceneId 进行加密,生成 EncryptedSceneId
     *
     * @param sceneId   验证码业务标识(原 CaptchaAppid)
     * @param ekeyStr   控制台获取的 ekey(作为密钥基础)
     * @param expireTimeSec  密文过期时间,单位秒,范围 1~86400
     * @return Base64   编码的字符串:EncryptedSceneId(IV + 加密数据)
     * @throws Exception   加密过程中可能出现异常(如不支持的算法)
     */
    public static String encryptSceneId(String sceneId, String ekeyStr, int expireTimeSec) throws Exception {
        if (expireTimeSec <= 0 || expireTimeSec > 86400) {
            throw new IllegalArgumentException("expireTimeSec must be between 1 and 86400 seconds.");
        }

        // 获取当前时间戳(秒级)
        long timestamp = Instant.now().getEpochSecond();

        // ==================== 步骤 1: 构造密钥 Key(32字节) ====================
        byte[] keyBytes = Base64.getDecoder().decode(ekeyStr);

        // ==================== 步骤 2: 构造明文并执行 AES-256-CBC 加密 ====================
        // 明文格式:sceneId&timestamp&expireTime
        String plaintext = sceneId + "&" + timestamp + "&" + expireTimeSec;
        byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);

        // 创建 Cipher 实例:AES/CBC/PKCS7Padding
        // 注意:Java 默认使用 PKCS5Padding,但其与 PKCS7 在 AES 中等价(块大小相同)
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // Java 不直接支持 PKCS7Padding,但行为一致

        // 随机生成 16 字节 IV
        SecureRandom random = new SecureRandom();
        byte[] iv = new byte[IV_LENGTH_BYTES];
        random.nextBytes(iv); // 安全随机填充 IV
        IvParameterSpec ivSpec = new IvParameterSpec(iv);

        // 使用 32 字节密钥初始化 SecretKeySpec
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");

        // 初始化 Cipher 为加密模式
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec);

        // 执行加密,得到密文字节数组(即 CaptchaSceneIdEncrypted)
        byte[] encryptedBytes = cipher.doFinal(plaintextBytes);

        // ==================== 步骤 3: 拼接 IV + 密文,并进行 Base64 编码 ====================
        // 创建一个新的字节数组:前 16 字节是 IV,后面是加密内容
        byte[] result = new byte[IV_LENGTH_BYTES + encryptedBytes.length];
        System.arraycopy(iv, 0, result, 0, IV_LENGTH_BYTES);
        System.arraycopy(encryptedBytes, 0, result, IV_LENGTH_BYTES, encryptedBytes.length);

        // Base64 编码,得到最终的 EncryptedSceneId
        return Base64.getEncoder().encodeToString(result);
    }

    // ==================== 测试示例 ====================

    public static void main(String[] args) {
        try {
            String sceneId = "s123456789"; // 替换为你的实际 sceneId
            String ekey = "your_key"; // 替换为控制台获取的实际 AppSecretKey
            int expireTimeSec = 3600; // 设置期望的有效期
            String encryptedSceneId = encryptSceneId(sceneId, ekey, expireTimeSec);
            
            System.out.println("EncryptedSceneId: " + encryptedSceneId);
            System.out.println("Length: " + encryptedSceneId.length() + " characters");

            // 示例输出(每次运行不同,因 IV 随机):
            // EncryptedSceneId: YmFzZTY0RW5jb2RlZFN0cmluZzEyczM0dTV2Nnd4eXo=
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

步骤二:前端集成

前端需先从业务服务端接口获取 EncryptedSceneId,然后在初始化验证码时传入该参数。

参数说明

参数

类型

是否必填

默认值

描述

EncryptedSceneId

String

加密后的 SceneId。创建验证场景后可获得原始 SceneId。使用控制台颁发的密钥(ekey),按照文档中的加密流程对该 SceneId 进行加密,最终得到的加密字符串即为加密后的 SceneId。

前端代码示例 (JavaScript)

// 1. 定义一个从您的业务后端获取加密字符串(EncryptedSceneId)的函数
const getEncryptedSceneId = async () => {
  // 此处应调用您自己实现的后端接口
  const response = await fetch('/api/encrypt-scene-id', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
  });
  // 假设后端返回JSON数据为:{ "EncryptedSceneId": "..." }
  const { EncryptedSceneId } = await response.json();
  return EncryptedSceneId;
};

// 2. 在初始化验证码时传入 EncryptedSceneId
const initCaptcha = async () => {
    const encryptedId = await getEncryptedSceneId();
    // 调用阿里云验证码初始化函数
    window.initAliyunCaptcha({
      // 场景ID。在新建验证场景后,可在验证码场景列表获取场景ID
      SceneId: "******",
      EncryptedSceneId: encryptedId,
      // ... 其他初始化参数
    });
};

// 执行初始化
initCaptcha();

步骤三:在控制台开启校验

在完成前后端代码的开发和测试后,可在验证码控制台为相应场景开启加密模式

  1. 登录阿里云验证码控制台,单击左侧导航栏的场景管理

  2. 场景管理页面,找到需要开启加密模式的目标场景,启用加密模式列下的开关。

image.png

开启后,验证码服务端将对所有传入 EncryptedSceneId 的请求执行以下强制校验:

  • 解密是否成功。

  • sceneId 是否合法。

  • timestamp 是否在允许的时间窗口内(防止重放攻击)。

  • 加密字符串是否在 expireTime 定义的有效期内。

说明

任何一项校验失败都会导致请求被拒绝。请务必在开启此开关前,确保您的服务端加密逻辑和前端集成已正确实现并完成测试。