主动人脸活体检测
证明摄像头前是真人:复核 eKYC SDK 在设备上采集的自拍与挑战记录,返回签名后的判定。
/v3/store/ekyc/face-active-liveness/finalize试一试- 价格1 IC每次请求
- 累计调用22.4K次
- v1.0
- 运行中
输入
- file
- selfie.jpg
- challenges
- {"session_id":"b0e7c1a2-4f5d-4e6a-9b8c-7d6e5f4a3b2c","sdk":{"name":"iapp-ekyc-sdk-flutter","version":"0.1.0","platform":"android"},"started_at":1767500000000,"finished_at":1767500008000,"challenges":[{"type":"blink","issued_at":1767500000123,"completed_at":1767500001873,"passed":true},{"type":"turn_left","issued_at":1767500002000,"completed_at":1767500004100,"passed":true},{"type":"smile","issued_at":1767500004500,"completed_at":1767500006900,"passed":true}]}
输出POST /v3/store/ekyc/face-active-liveness/finalize
- verdict
- { passed, passive_liveness, challenge_summary, session_id, selfie_sha256, timestamp, nonce }
- signature
- hex(HMAC-SHA256(secret, canonicalJSON(verdict)))
- signature_alg
- HMAC-SHA256
- process_time
- 0.42
请求
multipart/form-data,由 SDK 构建。若自行调用,需在设备上实现同等的挑战:从眨眼、左转、右转、微笑中随机选取,基于实时人脸关键点验证,并以真实时钟计时。
请求头
apikeystring必填请求体
filefile必填challengesstring必填session_id、sdk、started_at、finished_at,以及每个挑战一条记录,含 type、issued_at、completed_at 与 passed,如右侧面板所示。return_imagestringtrue 时在 selfie 字段中以 Base64 回传自拍。日志被接受的条件:每个类型都属于上述四种,至少发出两个挑战且全部通过,时间戳严格递增,每个挑战耗时 300 毫秒至 30 秒,整个会话不超过 120 秒,并在服务器时钟的 5 分钟内完成。
响应
完成的检查返回 200 并计费,即使 passed 为 false;错误响应从不计费。
verdictobjectverdict.passedbooleantrueverdict.passive_livenessobject对自拍的被动检测结果
verdict.passive_liveness.predictstringverdict.passive_liveness.real_scorenumberverdict.passive_liveness.thresholdnumberverdict.challenge_summaryobject已检查挑战的汇总;valid 为 false 时,reasons 列出原因
verdict.challenge_summary.totalintegerverdict.challenge_summary.passedintegerverdict.challenge_summary.typesarrayverdict.challenge_summary.duration_msintegerverdict.challenge_summary.validbooleanverdict.challenge_summary.reasonsarrayverdict.session_idverdict.timestampverdict.nonceverdict.selfie_sha256stringsignaturesignature_algverdict 规范化 JSON 的 HMAC-SHA256,密钥为您账户的签名密钥selfiereturn_image=true 时:filename、content_type、size、image_base64process_timenumber验证签名
在您的后端对 verdict 的规范形式重新计算 HMAC(键递归排序、无多余空白、UTF-8),并以恒定时间比较。然后检查 passed、timestamp 是否新鲜,若自拍单独传输,还需检查其 SHA-256 等于 selfie_sha256。
签名是响应 JSON 中的 signature 字段,不是 HTTP 头。密钥是您账户的签名密钥,显示在 API Keys 页面:每个账户一个,账户下所有 API Key 共用,且不同于任何 API Key。请只保存在服务器上;随应用或网页下发的密钥会让任何人都能伪造结果。
请按收到的原样传入响应体。被签名的文本使用 JavaScript 的数字格式(例如 0.00005 和 3.4e-7):先 JSON.parse 再 JSON.stringify 可以得到相同文本,但 Python 的 json.dumps 会写成 5e-05,导致 real_score 很小时验证失败。因此 Python 和 C# 示例保留响应体中数字的原始文本。三个示例均已用真实结果测试。
- Node.js
- Python
- C#
const crypto = require('crypto');
const sortKeysDeep = (v) =>
Array.isArray(v) ? v.map(sortKeysDeep)
: v && typeof v === 'object'
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeysDeep(v[k])]))
: v;
const canonical = (o) => JSON.stringify(sortKeysDeep(o));
function verifyVerdict(responseBody, accountSecret) {
const { verdict, signature } = JSON.parse(responseBody);
const expected = crypto.createHmac('sha256', accountSecret).update(canonical(verdict)).digest('hex');
return expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
}
// responseBody: the finalize response as a string; accountSecret: from the API Keys page
const ok = verifyVerdict(responseBody, accountSecret);
import hashlib
import hmac
import json
class _RawNumber(str):
"""A JSON number kept exactly as the server wrote it."""
def _canonical(value):
if isinstance(value, dict):
keys = sorted(value, key=lambda k: k.encode("utf-16-be")) # JavaScript sort order
return "{" + ",".join(
json.dumps(k, ensure_ascii=False) + ":" + _canonical(value[k]) for k in keys
) + "}"
if isinstance(value, list):
return "[" + ",".join(_canonical(v) for v in value) + "]"
if isinstance(value, _RawNumber):
return str(value)
return json.dumps(value, ensure_ascii=False) # strings, true, false, null
def verify_verdict(response_body: str, account_secret: str) -> bool:
"""response_body: the finalize response exactly as received (str)."""
resp = json.loads(response_body, parse_float=_RawNumber, parse_int=_RawNumber)
canonical = _canonical(resp["verdict"])
expected = hmac.new(account_secret.encode(), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, resp["signature"])
ok = verify_verdict(response.text, account_secret) # e.g. requests.Response.text
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public static class IappLivenessVerdict
{
// responseBody: the finalize response exactly as received. Do not deserialize it into
// a class and serialize it again; that can change numbers and key order.
// accountSecret: the Face Active Liveness Signing Secret from the iApp API Keys page.
public static bool Verify(string responseBody, string accountSecret)
{
using var doc = JsonDocument.Parse(responseBody);
var root = doc.RootElement;
var signature = root.GetProperty("signature").GetString() ?? "";
var canonical = new StringBuilder();
WriteCanonical(root.GetProperty("verdict"), canonical);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(accountSecret));
var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical.ToString()))).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(signature));
}
// Canonical JSON: object keys sorted by UTF-16 code unit at every level, no whitespace,
// numbers exactly as the server wrote them.
static void WriteCanonical(JsonElement e, StringBuilder sb)
{
switch (e.ValueKind)
{
case JsonValueKind.Object:
sb.Append('{');
var first = true;
foreach (var p in e.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
{
if (!first) sb.Append(',');
first = false;
WriteString(p.Name, sb);
sb.Append(':');
WriteCanonical(p.Value, sb);
}
sb.Append('}');
break;
case JsonValueKind.Array:
sb.Append('[');
var n = 0;
foreach (var item in e.EnumerateArray())
{
if (n++ > 0) sb.Append(',');
WriteCanonical(item, sb);
}
sb.Append(']');
break;
case JsonValueKind.String:
WriteString(e.GetString()!, sb);
break;
default: // number, true, false, null
sb.Append(e.GetRawText());
break;
}
}
// Escapes a string the way JavaScript's JSON.stringify does.
static void WriteString(string s, StringBuilder sb)
{
sb.Append('"');
foreach (var c in s)
{
switch (c)
{
case '"': sb.Append("\\\""); break;
case '\\': sb.Append("\\\\"); break;
case '\b': sb.Append("\\b"); break;
case '\f': sb.Append("\\f"); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default:
if (c < 0x20) sb.Append("\\u").Append(((int)c).ToString("x4"));
else sb.Append(c);
break;
}
}
sb.Append('"');
}
}
// var ok = IappLivenessVerdict.Verify(responseBody, accountSecret);
状态码
| 代码 | 含义 |
|---|---|
| 400 | INVALID_CHALLENGE_LOG、INVALID_IMAGE 或 MISSING_FIELD,附 reasons 数组 |
| 413 | 自拍超过 10 MB |
| 502 | UPSTREAM_UNAVAILABLE;稍后重试 |