腾讯云《意愿核身移动 H5》 快速完成身份验证接入
文章目录
- 简介
- 腾讯云《意愿核身移动 H5》 文档
- pom.xml
- 接口配置参数
- 获取 AccessToken
- 获取 Ticket
- 生成签名 sign
- 启动 H5 意愿核身
- 意愿核身查询结果
简介
本文将带你快速上手 腾讯云《意愿核身移动 H5》能力接入,基于 Java + SpringBoot 实现完整的接口调用流程。通过实战代码演示,涵盖了 获取 AccessToken、获取 Ticket、生成签名、启动 H5 意愿核身、查询结果回调 等关键环节,并结合 Hutool 工具类库 简化了 HTTP 请求和 JSON 处理,让接入过程更加高效。
在实名认证和远程身份核验场景中,意愿核身是保障业务安全合规的重要环节。本文基于腾讯云官方文档,整理出一套 后端直连接入 H5 意愿核身 的完整方案:
- 依赖配置:引入 hutool-all,简化请求与 JSON 解析;
- 接口实现:封装 getAccessToken、getApiTicket、sign 等工具方法;
- H5 启动:生成 FaceUrl 并跳转腾讯云意愿核身页面;
- 结果查询:回调获取 orderNo,调用 getWillFaceResult 查询最终结果;
通过本篇文章,你将能够快速完成腾讯云意愿核身的接入,避免掉入签名、参数拼接等常见坑,加速业务上线。
腾讯云《意愿核身移动 H5》 文档
- https://cloud.tencent.com/document/product/1007/77303
pom.xml
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.25</version>
</dependency>
接口配置参数
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;// Redis 缓存 Key
private final static String ACCESS_TOKEN_KEY = "face:tencent:access_token";
private final static String TICKET_KEY = "face:tencent:ticket";// 接口参数
private final static String BASE_URL = "https://kyc1.qcloud.com/api";
private final static String APP_ID = "xxxxxxxxx";
private final static String SECRET_KEY = "xxxxxxxxx";
private final static String VERSION = "1.0.0";// 意愿核身成功后回调 H5 页面地址
private final static String CALLBACK_URL = "https://test.domain/index";
private final static String QUESTION = "{0}是宇宙无敌帅。同意请回复“确认”,否则回复“不确认”。";
private final static String ANSWER = "确认";
获取 AccessToken
private String getAccessToken() {String accessToken = redisService.getCacheObject(ACCESS_TOKEN_KEY);if (accessToken != null) {return accessToken;}String url = MessageFormat.format(BASE_URL + "/oauth2/access_token?appId={0}&secret={1}&grant_type=client_credential&version={2}", APP_ID, SECRET_KEY, VERSION);HttpResponse response = HttpUtil.createGet(url).execute();String body = response.body();log.info("body: {}", body);JSONObject result = JSONUtil.parseObj(body);if (!result.containsKey("code") || !"0".equals(result.getStr("code"))) {throw new RuntimeException("获取Token失败:" + (result.containsKey("msg") ? result.getStr("msg") : "未知错误"));}accessToken = result.getStr("access_token");Long expireIn = result.getLong("expire_in");redisService.setCacheObject(ACCESS_TOKEN_KEY, accessToken, expireIn, TimeUnit.SECONDS);return accessToken;
}
获取 Ticket
private String getApiTicket() {String ticket = redisService.getCacheObject(TICKET_KEY);if (ticket != null) {return ticket;}String accessToken = getAccessToken();String url = MessageFormat.format(BASE_URL + "/oauth2/api_ticket?appId={0}&access_token={1}&type=SIGN&version={2}", APP_ID, accessToken, VERSION);HttpResponse response = HttpUtil.createGet(url).execute();String body = response.body();log.info("body: {}", body);JSONObject result = JSONUtil.parseObj(body);if (!result.containsKey("code") || !"0".equals(result.getStr("code"))) {throw new RuntimeException("获取Ticket失败:" + (result.containsKey("msg") ? result.getStr("msg") : "未知错误"));}JSONArray tickets = result.getJSONArray("tickets");if (tickets.isEmpty()) {throw new RuntimeException("获取Ticket失败,tickets为空");}JSONObject ticketJson = (JSONObject) tickets.get(0);ticket = ticketJson.getStr("value");Long expireIn = ticketJson.getLong("expire_in");redisService.setCacheObject(TICKET_KEY, ticket, expireIn, TimeUnit.SECONDS);return ticket;
}
生成签名 sign
private String sign(Map<String, Object> paramMap) {// 按值的字典序排List<Object> sortedValues = paramMap.values().stream().filter(Objects::nonNull).sorted(Comparator.comparing(Object::toString)).collect(Collectors.toList());String signStr = sortedValues.stream().map(Object::toString).collect(Collectors.joining(""));paramMap.remove("ticket");try {MessageDigest digest = MessageDigest.getInstance("SHA-1");byte[] hashBytes = digest.digest(signStr.getBytes(StandardCharsets.UTF_8));return bytesToHex(hashBytes).toUpperCase();} catch (NoSuchAlgorithmException e) {throw new RuntimeException("SHA-1 algorithm not available.", e);}
}private String bytesToHex(byte[] bytes) {StringBuilder hexString = new StringBuilder();for (byte b : bytes) {String hex = Integer.toHexString(0xff & b);if (hex.length() == 1) {hexString.append('0');}hexString.append(hex);}return hexString.toString();
}
启动 H5 意愿核身
public String getFaceUrl(String userId, String name, String idNo, Long depositAmount) {// 生成 32 位长度的订单号String orderNo = DateUtils.dateTimeNow() + StringUtils.getRandomString(18);String url = MessageFormat.format(BASE_URL + "/server/getWillFaceId?orderNo={0}", orderNo);Map<String, Object> paramMap = new HashMap<>();paramMap.put("appId", APP_ID);paramMap.put("userId", userId);paramMap.put("version", VERSION);// 生成 32 位长度的随机字符串paramMap.put("nonce", StringUtils.getRandomString(32));paramMap.put("ticket", getApiTicket());// 生成签名paramMap.put("sign", sign(paramMap));paramMap.put("name", name);paramMap.put("idNo", idNo);paramMap.put("orderNo", orderNo);paramMap.put("liveService", 2);// 参数值为1时,表示仅使用实时检测模式,不兼容的情况下回调错误码3005paramMap.put("liveInterType", 1);// 0:问答模式 1:播报模式 2:点头模式paramMap.put("willType", 0);paramMap.put("willLanguage", 0);// 系统播报问题文本/问题Map<String, String> questionMap = new LinkedHashMap<>();questionMap.put("id", "0");questionMap.put("question", MessageFormat.format(QUESTION, name));questionMap.put("answer", ANSWER);List<Map<String, String>> willContentList = new ArrayList<>();willContentList.add(questionMap);paramMap.put("willContentList", willContentList);paramMap.put("willMidAnswer", ANSWER);HttpRequest request = HttpUtil.createPost(url);request.header("Content-Type", "application/json");request.body(JSONUtil.toJsonStr(paramMap));HttpResponse response = request.execute();String body = response.body();log.info("body: {}", body);JSONObject result = JSONUtil.parseObj(body);if (!result.containsKey("code") || !"0".equals(result.getStr("code"))) {throw new RuntimeException("合作方后台上传信息失败:" + (result.containsKey("msg") ? result.getStr("msg") : "未知错误"));}// {// "code": "0",// "msg": "请求成功",// "bizSeqNo": "xxxxxxxxxxxxxxx",// "result": {// "bizSeqNo": "xxxxxxxxxxxxxxx",// "transactionTime": "20250903155605",// "oriCode": "0",// "orderNo": "xxxxxxxxxxxxxxx",// "faceId": "xxxxxxxxxxxxxxx",// "optimalDomain": "kyc1.qcloud.com",// "success": true// },// "transactionTime": "20250903155605"// }result = result.getJSONObject("result");orderNo = result.getStr("orderNo");String faceId = result.getStr("faceId");String optimalDomain = "kyc1.qcloud.com";if (result.containsKey("optimalDomain")) {optimalDomain = result.getStr("optimalDomain");}return willLogin(orderNo, userId, faceId, optimalDomain);
}private String willLogin(String orderNo, String userId, String faceId, String optimalDomain) {Map<String, Object> paramMap = new HashMap<>();paramMap.put("appId", APP_ID);paramMap.put("orderNo", orderNo);paramMap.put("userId", userId);paramMap.put("version", VERSION);paramMap.put("faceId", faceId);paramMap.put("ticket", getApiTicket());String nonceStr = StringUtils.getRandomString(32);paramMap.put("nonce", nonceStr);// 生成签名String signStr = sign(paramMap);try {String url = MessageFormat.format("https://{0}/api/web/willLogin?appId={1}&version={2}&nonce={3}&orderNo={4}&faceId={5}&url={6}&from=browser&userId={7}&sign={8}&redirectType=1",optimalDomain,APP_ID,VERSION,nonceStr,orderNo,faceId,// 回调时所带的参数:https://test.domain/index?orderNo=xxxx&code=0&newSignature=xxxx&liveRate=99&h5faceId=xxxx&willCode=0&faceCode=0&similarity=95.29URLEncoder.encode(CALLBACK_URL, String.valueOf(StandardCharsets.UTF_8)),userId,signStr);log.info("url: {}", url);return url;} catch (UnsupportedEncodingException e) {e.printStackTrace();log.error("{}", e.getMessage());throw new RuntimeException("启动 H5 意愿核身失败");}
}
注: 将生成的 FaceUrl 链接放到浏览器访问,就会跳到下方的意愿核身页面。
https://kyc1.qcloud.com/api/web/willLogin?appId=appId001
&version=1.0.0
&nonce=4bu6a5nv9t678m2t9je5819q46y9hf93
&orderNo=161709188560917432576916585
&faceId=wb04f10695c3651ce155fea7070b74c9
&url=https%3a%2f%2ftest.domain%2findex
&from=browser
&userId=23333333333333
&sign=5DD4184F4FB26B7B9F6DC3D7D2AB3319E5F7415F
&redirectType=1
意愿核身查询结果
public FaceResult getFaceResult(String orderNo) {String url = MessageFormat.format(BASE_URL + "/server/getWillFaceResult?orderNo={0}", orderNo);Map<String, Object> paramMap = new HashMap<>();paramMap.put("appId", APP_ID);paramMap.put("orderNo", orderNo);paramMap.put("version", VERSION);paramMap.put("ticket", getApiTicket());String nonceStr = StringUtils.getRandomString(32);paramMap.put("nonce", nonceStr);// 生成签名paramMap.put("sign", sign(paramMap));paramMap.put("getFile", 1);paramMap.put("getWillFile", 0);HttpRequest request = HttpUtil.createPost(url);request.header("Content-Type", "application/json");request.body(JSONUtil.toJsonStr(paramMap));HttpResponse response = request.execute();String body = response.body();log.info("body: {}", body);JSONObject result = JSONUtil.parseObj(body);if (!result.containsKey("code") || !"0".equals(result.getStr("code"))) {throw new RuntimeException("识别结果查询失败:" + (result.containsKey("msg") ? result.getStr("msg") : "未知错误"));}return JSONUtil.toBean(result.getJSONObject("result"), FaceResult.class);
}
注: orderNo 参数从 回调地址 参数中获取 https://test.domain/index?orderNo=xxxx&code=0&newSignature=xxxx&liveRate=99&h5faceId=xxxx&willCode=0&faceCode=0&similarity=95.29