签名校验与 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 都校验 |
各语言取秒级时间戳:
| 语言 | 写法 |
|---|---|
| Python | int(time.time()) |
| C# | DateTimeOffset.UtcNow.ToUnixTimeSeconds() |
| Go | time.Now().Unix() |
| Node.js | Math.floor(Date.now() / 1000) |
| Java | System.currentTimeMillis() / 1000 |
| C++ | std::time(nullptr) |
| 易语言 | 时间_取现行时间戳 () |
客户端时钟不准怎么办
虚拟机、长期休眠的笔记本、以及手动改过日期的机器都可能超出窗口。健壮做法:Init 失败且返回 1014 时,从 HTTP 响应头 Date 中取服务器时间,计算本地偏移量并在后续请求中补偿。
逐步自查清单(遇到 1010 时按顺序检查)
- Init 签名是否包含两次
AppKey? - 签名用的
TimeStamp和表单里提交的是否同一个值? Data提交的是 HEX 字符串,而不是原始二进制或 Base64?- Handle 签名的拼接顺序是
ApiPassword + TimeStamp + Data(不是Data + TimeStamp)? - MD5 输入是否按 UTF-8 编码?(某些语言默认 GBK / UTF-16)
- 参数值中的特殊字符是否做了 URL 编码?
ApiPassword是否是本次 Init 返回的最新值?