Skip to content

签名校验与 RC4 加密

A7验证用 RC4 对称加密 保护业务参数,用 MD5 签名 防止参数被篡改,用 ±300 秒时间戳窗口 限制重放。本页把三者讲透。

两条签名公式

Init 阶段

text
Sign = MD5(AppId + AppKey + TimeStamp + AppKey)

AppKey 出现两次

AppKey 分别位于中间结尾。这是最容易写错的地方,漏掉结尾那次就会返回 1010 签名错误

举例:

text
AppId     = 150018
AppKey    = ABCDEF0123456789ABCDEF0123456789
TimeStamp = 1780000000

待签名串 = "150018ABCDEF0123456789ABCDEF01234567891780000000ABCDEF0123456789ABCDEF0123456789"
Sign     = MD5(待签名串)  → 32 位小写十六进制

Handle 阶段

text
Data = HEX(RC4(ApiPassword, 明文参数串))
Sign = MD5(ApiPassword + TimeStamp + Data)

举例:

text
ApiPassword = A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6
TimeStamp   = 1780000000
明文参数     = "action=SingleLogin&Card=UTDE-7TCX-N5AT-ZDSP&Mac=TEST-ABC-123"
Data        = "1F3B7C90AE..."   (RC4 密文转 HEX)

待签名串 = "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P61780000000" + Data
Sign     = MD5(待签名串)

签名与提交必须用同一个时间戳

不要「先算签名,再重新取一次时间戳提交」。请先把 ts 存进变量,签名和表单字段都用这个变量。这是 1010 最常见的成因之一。

MD5 约定

项目约定
输出格式32 位十六进制字符串
大小写推荐小写;服务端做严格字符串比对,请统一小写
输入编码UTF-8 字节
是否加盐否,公式即全部输入

RC4 加密约定

项目约定
算法标准 RC4(KSA + PRGA,无 drop-n)
密钥ApiPassword 的 UTF-8 字节
明文参数串的 UTF-8 字节
输出编码密文字节转十六进制字符串,推荐大写
对称性加密与解密是同一个运算

标准 RC4 参考实现

python
def rc4_hex(key: str, plain: str) -> str:
    """RC4 加密,返回大写 HEX 字符串。"""
    key_bytes = key.encode("utf-8")
    s = list(range(256))
    j = 0
    # KSA: 密钥调度算法
    for i in range(256):
        j = (j + s[i] + key_bytes[i % len(key_bytes)]) % 256
        s[i], s[j] = s[j], s[i]
    # PRGA: 伪随机生成算法
    i = j = 0
    out = bytearray()
    for b in plain.encode("utf-8"):
        i = (i + 1) % 256
        j = (j + s[i]) % 256
        s[i], s[j] = s[j], s[i]
        out.append(b ^ s[(s[i] + s[j]) % 256])
    return out.hex().upper()
csharp
public static string Rc4Hex(string key, string plain)
{
    byte[] keyBytes = Encoding.UTF8.GetBytes(key);
    byte[] data = Encoding.UTF8.GetBytes(plain);
    int[] s = new int[256];
    for (int i = 0; i < 256; i++) s[i] = i;

    // KSA
    for (int i = 0, j = 0; i < 256; i++)
    {
        j = (j + s[i] + keyBytes[i % keyBytes.Length]) % 256;
        (s[i], s[j]) = (s[j], s[i]);
    }

    // PRGA
    var sb = new StringBuilder(data.Length * 2);
    for (int k = 0, x = 0, y = 0; k < data.Length; k++)
    {
        x = (x + 1) % 256;
        y = (y + s[x]) % 256;
        (s[x], s[y]) = (s[y], s[x]);
        sb.Append((data[k] ^ s[(s[x] + s[y]) % 256]).ToString("X2"));
    }
    return sb.ToString();
}
go
// Rc4Hex 使用标准 RC4 加密并返回大写 HEX 字符串。
func Rc4Hex(key, plain string) string {
	k := []byte(key)
	data := []byte(plain)
	var s [256]int
	for i := 0; i < 256; i++ {
		s[i] = i
	}
	// KSA
	j := 0
	for i := 0; i < 256; i++ {
		j = (j + s[i] + int(k[i%len(k)])) % 256
		s[i], s[j] = s[j], s[i]
	}
	// PRGA
	out := make([]byte, len(data))
	x, y := 0, 0
	for n := 0; n < len(data); n++ {
		x = (x + 1) % 256
		y = (y + s[x]) % 256
		s[x], s[y] = s[y], s[x]
		out[n] = data[n] ^ byte(s[(s[x]+s[y])%256])
	}
	return strings.ToUpper(hex.EncodeToString(out))
}
javascript
/** RC4 加密,返回大写 HEX 字符串。 */
function rc4Hex(key, plain) {
  const k = Buffer.from(key, 'utf8')
  const data = Buffer.from(plain, 'utf8')
  const s = new Array(256)
  for (let i = 0; i < 256; i++) s[i] = i

  // KSA
  for (let i = 0, j = 0; i < 256; i++) {
    j = (j + s[i] + k[i % k.length]) % 256
    ;[s[i], s[j]] = [s[j], s[i]]
  }

  // PRGA
  const out = Buffer.alloc(data.length)
  for (let n = 0, x = 0, y = 0; n < data.length; n++) {
    x = (x + 1) % 256
    y = (y + s[x]) % 256
    ;[s[x], s[y]] = [s[y], s[x]]
    out[n] = data[n] ^ s[(s[x] + s[y]) % 256]
  }
  return out.toString('hex').toUpperCase()
}

明文参数串的拼接规则

text
action=<ActionName>&<Key1>=<Value1>&<Key2>=<Value2>
规则说明
action 位置建议置于首位(服务端不强制,但便于排查)
分隔符键值对之间用 &,键与值之间用 =
键名大小写区分大小写,例如 UserName 不能写成 username(部分字段有兼容别名,见各接口页)
值的编码若值中含 & = % + 等符号,需先做 URL 编码
空值可选参数不传即可,不要传空串
中文支持,按 UTF-8 编码后再 URL 编码

密码里有特殊字符怎么办

服务端用 parse_str 解析明文串,其行为等价于解析 URL query。所以密码如果包含 &,必须写成 %26,否则会被截断。统一对所有参数值做 URL 编码是最安全的做法。

时间戳窗口

项目
单位(Unix timestamp,不是毫秒)
允许偏差±300 秒(前后各 5 分钟)
超出返回1014 时间戳已过期
校验位置Init 与 Handle 都校验

各语言取秒级时间戳:

语言写法
Pythonint(time.time())
C#DateTimeOffset.UtcNow.ToUnixTimeSeconds()
Gotime.Now().Unix()
Node.jsMath.floor(Date.now() / 1000)
JavaSystem.currentTimeMillis() / 1000
C++std::time(nullptr)
易语言时间_取现行时间戳 ()

客户端时钟不准怎么办

虚拟机、长期休眠的笔记本、以及手动改过日期的机器都可能超出窗口。健壮做法:Init 失败且返回 1014 时,从 HTTP 响应头 Date 中取服务器时间,计算本地偏移量并在后续请求中补偿。

逐步自查清单(遇到 1010 时按顺序检查)

  1. Init 签名是否包含两次 AppKey
  2. 签名用的 TimeStamp 和表单里提交的是否同一个值
  3. Data 提交的是 HEX 字符串,而不是原始二进制或 Base64?
  4. Handle 签名的拼接顺序是 ApiPassword + TimeStamp + Data(不是 Data + TimeStamp)?
  5. MD5 输入是否按 UTF-8 编码?(某些语言默认 GBK / UTF-16)
  6. 参数值中的特殊字符是否做了 URL 编码?
  7. ApiPassword 是否是本次 Init 返回的最新值?

相关链接

A7验证 · 软件授权与网络验证平台