
不是所有业务都有原生 App。H5 方式让人脸核身可以直接在网页里完成,无需开发客户端。本文讲清楚 H5 核身的适用条件、接入流程、签名生成方法和代码示例。
H5 人脸核身适合以下情况:
H5 方式依赖浏览器调用摄像头,因此有明确的运行前提:
浏览器 | 最低版本 |
|---|---|
Chrome | 53+ |
Firefox | 29+ |
Safari | iOS 11+ |
微信内置浏览器 | 6.5+ |
注意:部分浏览器不支持实时检测模式,需要引导用户更换。如果当前设备无法满足条件,前端会返回特定的错误码。
前端 → 业务服务端 → 慧眼服务端
↓ ↓ ↓
请求核身 获取ticket 返回h5faceId
↓ ↓ ↓
计算签名 ← 返回签名参数
↓
跳转核身页面
↓
用户完成活体检测
↓
回调业务页面(带token)
↓
服务端拉取结果步骤 | 执行方 | 动作 |
|---|---|---|
1 | 前端 | 用户填写身份信息,点击开始核身 |
2 | 业务服务端 | 调用 |
3 | 业务服务端 | 获取 NONCE 类型 ticket |
4 | 业务服务端 | 计算签名 sign 并返回给前端 |
5 | 前端 | 构建核身 URL 并跳转 |
6 | 用户 | 完成活体检测(眨眼、摇头等) |
7 | 核身页面 | 回调业务指定 URL,携带 token |
8 | 业务服务端 | 调用 |
参数 | 说明 | 来源 |
|---|---|---|
appId | 业务流程唯一标识(WBappid) | 控制台申请 |
orderNo | 订单号,字母/数字组成 | 业务方分配 |
userId | 用户唯一标识 | 业务方分配 |
version | 版本号,固定值 | - |
h5faceId | 接口返回的唯一标识 | geth5faceid 接口返回 |
ticket | NONCE 类型 ticket | 服务端实时获取 |
nonce | 32位随机字符串 | 业务方生成 |
1. 生成一个32位的随机字符串 nonce(字母+数字)
2. 将 appId、userId、orderNo、version、h5faceId、ticket、nonce 共7个参数的值进行字典序排序
3. 将排序后的所有参数字符串拼接成一个字符串
4. 对拼接后的字符串进行 SHA1 编码,得到40位签名 signimport hashlib
import random
import string
def generate_nonce(length=32):
"""生成32位随机字符串"""
chars = string.ascii_letters + string.digits
return ''.join(random.choice(chars) for _ in range(length))
def generate_sign(params):
"""
生成签名
params: dict,包含 appId, userId, orderNo, version, h5faceId, ticket, nonce
"""
# 1. 按字典序排序
sorted_keys = sorted(params.keys())
# 2. 拼接所有参数值
concatenated = ''.join(params[key] for key in sorted_keys)
# 3. SHA1 编码
sign = hashlib.sha1(concatenated.encode('utf-8')).hexdigest().upper()
return sign
# 示例参数
params = {
'appId': 'appId001',
'userId': 'userID19959248596551',
'orderNo': 'aabc1457895464',
'version': '1.0.0',
'h5faceId': 'bwiwe1457895464',
'ticket': 'zxc9Qfxlti9iTVgHAjwvJdAZKN3nMuUhrsPdPlPVKlcyS50N6tlLnfuFBPIucaMS',
'nonce': 'kHoSxvLZGxSoFsjxlbzEoUzh5PAnTU7T'
}
sign = generate_sign(params)
print(f"签名: {sign}")
# 输出: 4E9DFABF938BF37BDB7A7DC25CCA1233D12D986Bconst crypto = require('crypto');
function generateSign(params) {
// 1. 按字典序排序
const sortedKeys = Object.keys(params).sort();
// 2. 拼接所有参数值
const concatenated = sortedKeys.map(key => params[key]).join('');
// 3. SHA1 编码
const sign = crypto.createHash('sha1').update(concatenated).digest('hex').toUpperCase();
return sign;
}
// 示例参数
const params = {
appId: 'appId001',
userId: 'userID19959248596551',
orderNo: 'aabc1457895464',
version: '1.0.0',
h5faceId: 'bwiwe1457895464',
ticket: 'zxc9Qfxlti9iTVgHAjwvJdAZKN3nMuUhrsPdPlPVKlcyS50N6tlLnfuFBPIucaMS',
nonce: 'kHoSxvLZGxSoFsjxlbzEoUzh5PAnTU7T'
};
const sign = generateSign(params);
console.log(`签名: ${sign}`);# Python 示例:调用 geth5faceid 接口
import requests
def get_h5_face_id(app_id, user_id, order_no, name, id_card):
"""
上送身份信息,获取 h5faceId
"""
url = "https://miniprogram-kyc.tencentcloudapi.com/api/server/h5/geth5faceid"
# 生成签名(这里简化,实际需要按上述方法生成)
nonce = generate_nonce()
params = {
'appId': app_id,
'userId': user_id,
'orderNo': order_no,
'version': '1.0.0',
'name': name,
'idCard': id_card,
'nonce': nonce,
'ticket': get_nonce_ticket(user_id) # 从腾讯云服务获取
}
params['sign'] = generate_sign(params)
response = requests.post(url, json=params)
result = response.json()
if result.get('code') == 0:
return {
'h5faceId': result['data']['h5faceId'],
'optimalDomain': result['data'].get('optimalDomain', 'kyc1.qcloud.com')
}
else:
raise Exception(f"获取h5faceId失败: {result.get('msg')}")def get_nonce_ticket(user_id):
"""
获取 NONCE 类型的 ticket
有效期120秒,一次性有效
"""
# 调用腾讯云服务获取 ticket
# 具体接口请参考官方文档
url = "https://miniprogram-kyc.tencentcloudapi.com/api/server/getTicket"
params = {
'appId': APP_ID,
'userId': user_id,
'type': 'NONCE'
}
response = requests.post(url, json=params)
result = response.json()
if result.get('code') == 0:
return result['data']['ticket']
else:
raise Exception(f"获取ticket失败: {result.get('msg')}")/**
* 构建 H5 人脸核身 URL
*/
function buildVerifyUrl(params) {
const { appId, version, nonce, orderNo, h5faceId, userId, sign, callbackUrl, optimalDomain } = params;
// 对回调地址进行 URL Encode
const encodedUrl = encodeURIComponent(callbackUrl);
// 构建核身 URL
const domain = optimalDomain || 'kyc1.qcloud.com';
const verifyUrl = `https://${domain}/api/pc/login?` +
`appId=${appId}` +
`&version=${version}` +
`&nonce=${nonce}` +
`&orderNo=${orderNo}` +
`&h5faceId=${h5faceId}` +
`&url=${encodedUrl}` +
`&userId=${userId}` +
`&sign=${sign}`;
return verifyUrl;
}/**
* 启动 H5 人脸核身
*/
async function startFaceVerify(userId, name, idCard) {
try {
// 1. 请求服务端获取核身参数
const res = await fetch('/api/getFaceVerifyParams', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, name, idCard })
});
const data = await res.json();
if (data.code !== 0) {
throw new Error(data.msg || '获取核身参数失败');
}
const { appId, version, nonce, orderNo, h5faceId, sign, optimalDomain } = data.data;
// 2. 构建回调地址
const callbackUrl = `${window.location.origin}/pages/verify-result`;
// 3. 构建核身 URL 并跳转
const verifyUrl = buildVerifyUrl({
appId, version, nonce, orderNo, h5faceId, userId, sign, callbackUrl, optimalDomain
});
// 注意:不能直接用 <a> 标签跳转,会导致签名被预加载而失效
// 应使用以下方式跳转
window.location.href = verifyUrl;
} catch (error) {
console.error('启动核身失败:', error);
alert(error.message);
}
}
// 用户点击按钮触发
document.getElementById('btn-verify').addEventListener('click', () => {
const userId = document.getElementById('user-id').value;
const name = document.getElementById('name').value;
const idCard = document.getElementById('id-card').value;
if (!userId || !name || !idCard) {
alert('请填写完整信息');
return;
}
startFaceVerify(userId, name, idCard);
});// 在回调页面 /pages/verify-result 中处理结果
window.addEventListener('DOMContentLoaded', () => {
// 从 URL 中获取 token 参数
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
// 核身完成,通知服务端拉取结果
getVerifyResult(token);
} else {
// 未获取到 token,可能用户中途退出
showErrorMessage('未完成核身');
}
});
async function getVerifyResult(token) {
try {
const res = await fetch('/api/getVerifyResult', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const data = await res.json();
if (data.code === 0 && data.data.success) {
// 核身成功
showSuccessPage(data.data);
} else {
// 核身失败
showErrorMessage(data.data?.message || '核身未通过');
}
} catch (error) {
console.error('获取结果失败:', error);
showErrorMessage('网络异常,请重试');
}
}返回码 | 含义 | 处理建议 |
|---|---|---|
3001 | 当前浏览器不支持视频录制 | 引导更换浏览器或使用备用方案 |
3002 | 登录态异常,cookie 参数缺失 | 引导用户重新进入 |
3003 | 核身中途中断 | 引导重新发起 |
3004 | 无摄像头权限 | 引导重新授权摄像头 |
3005 | 当前浏览器不支持实时检测模式 | 建议更换浏览器 |
300101 | 报文包体问题 | 提示用户重新进入 |
签名不合法 | 签名计算错误或参数不一致 | 检查签名算法和参数 |
场景 | 推荐方案 |
|---|---|
对核身体验要求较高 | 原生 App SDK |
用户设备环境复杂 | 原生 App SDK |
核身是低频补充环节 | H5(成本优势明显) |
微信生态内高频使用 | 微信小程序 |
选型时应先确认目标渠道是否在支持列表内:
腾讯云慧眼人脸核身支持微信 H5、PC 浏览器 H5 等多种接入渠道,可根据业务形态选择合适的方式。该系列产品正在限时特惠活动中,低至3.3折:https://cloud.tencent.com/act/pro/happynewyears
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。