服务端实现
宿主服务端在响应前端「跳转参数请求」时,需完成两件事: ① 加密用户档案(AES-256-CBC),② 生成跳转签名(HMAC-SHA256)。 两者都以 appSecret 作为密钥,禁止在前端执行。
签名流程概览
请求进来
↓
1. 构造用户档案 JSON
↓
2. AES-256-CBC 加密 → userProfile (base64)
↓
3. 生成 timestamp (10位Unix秒) + nonce (随机)
↓
4. HMAC-SHA256(appSecret, "appKey|thirdUserId|timestamp|nonce") → sign
↓
5. 返回 { appKey, thirdUserId, userProfile, timestamp, nonce, sign, deviceKey, measureMode }档案加密算法
key = SHA256(appSecret) // 32 字节 binary
iv = random(16 字节) // 每次调用重新生成
明文 JSON = JSON.stringify(profileObject) // UTF-8
ciphertext = AES_256_CBC(key, iv, 明文, PKCS7_Padding)
userProfile = base64( iv[16B] ++ ciphertext )userProfile 字段说明
| 字段 | 类型 | 必填条件 | 说明 |
|---|---|---|---|
thirdUserId | string | ✅ 始终 | 与跳转参数 thirdUserId 一致 |
name | string | 否 | 姓名,结果页展示 |
gender | string | BCA 必填 | MALE / FEMALE / UNKNOWN |
birth_date | string | BCA 必填 | YYYY-MM-DD |
height | int | BCA 必填 | 单位 cm(50–300),缺失报 2003 |
weight | int | 否 | 单位 kg×100,如 70.5 kg → 7050 |
phone | string | 否 | 11 位手机号;有值时服务端自动完成用户主档绑定 |
BCA 自动转换:体脂秤场景下,服务端从 userProfile 中提取
height / age / gender / weight并转换为 SDK 口径返回给插件,宿主无需额外传这四个明文字段。跳转签名算法
signString = appKey + "|" + thirdUserId + "|" + timestamp + "|" + nonce
sign = HMAC_SHA256(appSecret, signString).toLowerCase()timestamp必须是 10 位 Unix 秒(Math.floor(Date.now() / 1000)),非毫秒- 签名字段固定为 4 个(顺序固定):
appKey、thirdUserId、timestamp、nonce userProfile、deviceKey、measureMode不参与签名- sign 输出为 hex 小写字符串
Node.js 示例
const crypto = require('crypto')
// AES-256-CBC 加密用户档案
function encryptUserProfile(appSecret, profile) {
const key = crypto.createHash('sha256').update(appSecret).digest() // 32 bytes
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
const plaintext = JSON.stringify(profile)
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
return Buffer.concat([iv, encrypted]).toString('base64')
}
// HMAC-SHA256 签名
function generateSign(appSecret, appKey, thirdUserId, timestamp, nonce) {
const signString = [appKey, thirdUserId, timestamp, nonce].join('|')
return crypto.createHmac('sha256', appSecret).update(signString).digest('hex').toLowerCase()
}
// 完整跳转参数
function buildPluginParams({ appKey, appSecret, thirdUserId, deviceKey, measureMode, profile }) {
const timestamp = String(Math.floor(Date.now() / 1000)) // 10 位秒级
const nonce = crypto.randomBytes(8).toString('hex') // 16 位十六进制
const userProfile = encryptUserProfile(appSecret, { thirdUserId, ...profile })
const sign = generateSign(appSecret, appKey, thirdUserId, timestamp, nonce)
return { appKey, thirdUserId, userProfile, timestamp, nonce, sign, deviceKey, measureMode }
}
// 示例(Express 路由)
app.post('/api/xiaobao/plugin-params', (req, res) => {
const { thirdUserId, deviceKey, measureMode } = req.body
const params = buildPluginParams({
appKey: process.env.XIAOBAO_APP_KEY,
appSecret: process.env.XIAOBAO_APP_SECRET,
thirdUserId,
deviceKey,
measureMode,
profile: {
thirdUserId,
name: req.user.name,
gender: 'MALE', // MALE / FEMALE / UNKNOWN
birth_date: '1985-06-15', // YYYY-MM-DD
height: 175, // cm
weight: 7050, // kg×100,70.5kg → 7050
phone: req.user.phone, // 可选
},
})
res.json({ code: 0, data: params })
})PHP 示例
function encryptUserProfile(string $appSecret, array $profile): string {
$key = hash('sha256', $appSecret, true); // 32 bytes binary
$iv = random_bytes(16);
$plaintext = json_encode($profile, JSON_UNESCAPED_UNICODE);
$ciphertext = openssl_encrypt($plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv . $ciphertext); // base64(iv[16B] + ciphertext)
}
function generateSign(string $appSecret, string $appKey, string $thirdUserId, string $timestamp, string $nonce): string {
$signString = implode('|', [$appKey, $thirdUserId, $timestamp, $nonce]);
return strtolower(hash_hmac('sha256', $signString, $appSecret));
}
// 示例
$appKey = getenv('XIAOBAO_APP_KEY');
$appSecret = getenv('XIAOBAO_APP_SECRET');
$thirdUserId = 'user_10086';
$profile = [
'thirdUserId' => $thirdUserId,
'name' => '张三',
'gender' => 'MALE', // MALE / FEMALE / UNKNOWN
'birth_date' => '1985-06-15', // YYYY-MM-DD
'height' => 175, // cm
'weight' => 7050, // kg×100
'phone' => '13800138000', // 可选
];
$timestamp = (string) time(); // 10 位 Unix 秒
$nonce = bin2hex(random_bytes(8));
$userProfile = encryptUserProfile($appSecret, $profile);
$sign = generateSign($appSecret, $appKey, $thirdUserId, $timestamp, $nonce);
return [
'appKey' => $appKey,
'thirdUserId' => $thirdUserId,
'userProfile' => $userProfile,
'timestamp' => $timestamp,
'nonce' => $nonce,
'sign' => $sign,
'deviceKey' => 'HOME_MULTI_BIO_MONITOR',
'measureMode' => 'BP',
];