服务端实现

宿主服务端在响应前端「跳转参数请求」时,需完成两件事: ① 加密用户档案(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 字段说明

字段类型必填条件说明
thirdUserIdstring✅ 始终与跳转参数 thirdUserId 一致
namestring姓名,结果页展示
genderstringBCA 必填MALE / FEMALE / UNKNOWN
birth_datestringBCA 必填YYYY-MM-DD
heightintBCA 必填单位 cm(50–300),缺失报 2003
weightint单位 kg×100,如 70.5 kg → 7050
phonestring11 位手机号;有值时服务端自动完成用户主档绑定
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 个(顺序固定):appKeythirdUserIdtimestampnonce
  • userProfiledeviceKeymeasureMode 不参与签名
  • 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',
];