腾讯云人脸核身文档
最近公司有业务需求,需要对企业微信中的小程序添加人脸识别功能,一般的人脸核身是对app中添加sdk完成的,考虑到业务需要,采用腾讯云的移动浮层H5接入,废话不多说,直接上代码。
这边,这3步已经满足了我们的需要。
nonce是自定义的随机字符串,redirectUrl是验证成功后回调的页面
# 腾讯人脸识别配置
tencentFaceVerify:
appId: ******
appSecret: ***********************
version: 1.0.0
nonce: ************
redirectUrl: https://*****/index
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Base64;
public class Base64Util {
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public Base64Util() {
}
public static String encode(String source) {
if (!isEmpty(source)) {
byte[] bytes = source.getBytes(DEFAULT_CHARSET);
String asB64 = Base64.getEncoder().encodeToString(bytes);
return asB64;
} else {
return source;
}
}
public static String decode(String source) {
if (!isEmpty(source)) {
byte[] bytes = Base64.getDecoder().decode(source);
String target = new String(bytes, DEFAULT_CHARSET);
return target;
} else {
return source;
}
}
private static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
/**
* 对字节数组字符串进行Base64解码并生成图片
* @param base64
* @param path
* @return
*/
public static boolean base64ToImage(String base64, String path) {
if (base64 == null) {
return false;
}
BASE64Decoder decoder = new BASE64Decoder();
try {
// Base64解码
byte[] bytes = decoder.decodeBuffer(base64);
for (int i = 0; i < bytes.length; ++i) {
if (bytes[i] < 0) {// 调整异常数据
bytes[i] += 256;
}
}
// 生成jpeg图片
OutputStream out = new FileOutputStream(path);
out.write(bytes);
out.flush();
out.close();
return true;
} catch (Exception e) {
return false;
}
}
/**
* 将一张网络图片转化成Base64字符串
* @param imgURL
* @return
*/
public static String GetImageStrFromUrl(String imgURL) {
ByteArrayOutputStream data = new ByteArrayOutputStream();
try {
// 创建URL
URL url = new URL(imgURL);
byte[] by = new byte[1024];
// 创建链接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
InputStream is = conn.getInputStream();
// 将内容读取内存中
int len = -1;
while ((len = is.read(by)) != -1) {
data.write(by, 0, len);
}
// 关闭流
is.close();
} catch (IOException e) {
e.printStackTrace();
}
// 对字节数组Base64编码
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(data.toByteArray());
}
}
package com.lynkco.lcem.util;
import java.text.SimpleDateFormat;
import java.util.UUID;
public class SerialUtil {
private static final String DATE_FORMAT = "yyyyMMdd";
private SerialUtil (){}
public static String getBatchNo(String prefix) {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
StringBuffer batchNo = new StringBuffer(prefix);
batchNo.append(sdf.format(System.currentTimeMillis()))
.append((int)((Math.random()*9+1)*100000));
return batchNo.toString();
}
public static String getUUID() {
String uuid = UUID.randomUUID().toString();
return uuid.replace("-", "");
}
}
/**
* @author :xuwei
* @description:人脸入参,参考https://cloud.tencent.com/document/product/1007/61073
* @date :2022/8/25 21:43
*/
@Data
@ApiModel
public class TencentFaceVerifyParam {
@ApiModelProperty(name="sourcePhotoStr", value = "比对源照片")
private String sourcePhotoStr;
@ApiModelProperty(name="sourcePhotoType", value = "比对源照片类型参数值为1 时是:水纹正脸照 2时是:高清正脸照")
private String sourcePhotoType;
@ApiModelProperty(name="liveInterType", value = "参数值为1时,表示仅使用实时检测模式, " +
"参数值非1或不入参,表示优先使用实时检测模式,如遇不兼容情况,自动降级为视频录制模式")
private String liveInterType;
}
/**
* @author :xuwei
* @description:人脸入参,参考https://cloud.tencent.com/document/product/1007/61073
* @date :2022/8/25 21:43
*/
@Data
@Builder
public class TencentFaceVerifyDto {
private String appId;
private String orderNo;
private String userId;
private String sourcePhotoStr;
private String sourcePhotoType;
private String liveInterType;
private String version;
private String sign;
private String nonce;
private String ticket;
}
/**
* @author :xuwei
* @description:人脸出参,参考https://cloud.tencent.com/document/product/1007/61073
* https://cloud.tencent.com/document/product/1007/61074
* @date :2022/8/25 21:43
*/
@Data
public class TencentFaceVerifyResult {
private String bizSeqNo;
private String transactionTime;
private String orderNo;
private String faceId;
private String optimalDomain;
private String success;
private String webankAppId;
private String userId;
private String version;
private String nonce;
private String sign;
private String redirectUrl;
}
controller
import com.lynkco.lcem.common.BaseResult;
import com.lynkco.lcem.common.config.LoginUserUtil;
import com.lynkco.lcem.dto.tencentFaceVerify.TencentFaceVerifyDto;
import com.lynkco.lcem.dto.tencentFaceVerify.TencentFaceVerifyParam;
import com.lynkco.lcem.dto.tencentFaceVerify.TencentFaceVerifyResult;
import com.lynkco.lcem.service.common.TencentFaceVerifyService;
import com.lynkco.lcem.util.ObjectUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* tencent实人认证
*
* @author xuwei
* @title: TencentFaceVerifyController
* @projectName lcem-server
* @description: TODO
* @date 2022/8/23 09:00
*/
@RefreshScope
@Slf4j
@RestController
@RequestMapping("/tencentFaceVerify")
@Api(value = "腾讯实人认证", tags = {"腾讯实人认证"})
public class TencentFaceVerifyController {
@Autowired
TencentFaceVerifyService tencentFaceVerifyService;
@ApiOperation("上送身份信息")
@PostMapping("/getAdvFaceId")
public BaseResult getAdvFaceId(@RequestBody TencentFaceVerifyParam param, HttpServletRequest request) {
String userId = LoginUserUtil.getCurrentUserId(request);
TencentFaceVerifyDto dto = TencentFaceVerifyDto.builder().sourcePhotoStr(param.getSourcePhotoStr())
.sourcePhotoType(param.getSourcePhotoType()).liveInterType(param.getLiveInterType())
.userId(userId).build();
String advFaceId = tencentFaceVerifyService.getAdvFaceId(dto);
if (!ObjectUtils.isEmpty(advFaceId)){
return BaseResult.ok(advFaceId);
}
return BaseResult.ok(null);
}
}
service
public interface TencentFaceVerifyService {
/**
* 上送身份信息
* @param param
* @return
*/
String getAdvFaceId(TencentFaceVerifyDto param);
}
serviceImpl
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import com.lynkco.lcem.dto.tencentFaceVerify.TencentFaceVerifyDto;
import com.lynkco.lcem.dto.tencentFaceVerify.TencentFaceVerifyResult;
import com.lynkco.lcem.exception.BusinessException;
import com.lynkco.lcem.util.Base64Util;
import com.lynkco.lcem.util.HttpClientUtil;
import com.lynkco.lcem.util.SerialUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Service;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* @author :xuwei
* @description:TODO
* @date :2022/8/24 16:39
*/
@RefreshScope
@Service
public class TencentFaceVerifyServiceImpl implements TencentFaceVerifyService {
@Value("${tencentFaceVerify.appId}")
private String appId;
@Value("${tencentFaceVerify.appSecret}")
private String appSecret;
@Value("${tencentFaceVerify.version}")
private String version;
@Value("${tencentFaceVerify.nonce}")
private String nonce;
@Value("${tencentFaceVerify.redirectUrl}")
private String redirectUrl;
/**
* 上送身份信息
* 参考文档:https://cloud.tencent.com/document/product/1007/61073
* @param param
* @return
*/
@Override
public String getAdvFaceId(TencentFaceVerifyDto param) {
String accessToken = getAccessToken();
String signTickets = getSignTickets(accessToken);
String encode = Base64Util.GetImageStrFromUrl(param.getSourcePhotoStr());
param.setSign(beforeSign(param.getUserId(), signTickets));
param.setSourcePhotoStr(encode);
param.setOrderNo(SerialUtil.getUUID());
param.setAppId(appId);
param.setVersion(version);
param.setNonce(nonce);
try {
String jsonStr = JSONUtil.toJsonStr(param);
String result = HttpClientUtil.doPostJson("https://kyc.qcloud.com/api/server/getAdvFaceId", jsonStr, null);
Map<String, Object> resultMap = JSONObject.parseObject(result, Map.class);
if (resultMap.get("code").equals("0")) {
TencentFaceVerifyResult verifyResult = JSONObject.parseObject(resultMap.get("result").toString(), TencentFaceVerifyResult.class);
verifyResult.setUserId(param.getUserId());
//启动 H5 人脸核身地址
String url = getUrl(param, accessToken, verifyResult);
return url;
}
} catch (Exception e) {
throw new BusinessException("后台上送身份信息失败:" + e.getMessage());
}
return null;
}
/**
* 启动 H5 人脸核身地址
* 参考文档:https://cloud.tencent.com/document/product/1007/61074
* @param param
* @param accessToken
* @param verifyResult
* @return
* @throws UnsupportedEncodingException
*/
private String getUrl(TencentFaceVerifyDto param, String accessToken, TencentFaceVerifyResult verifyResult) throws UnsupportedEncodingException {
String nonceTickets = getNonceTickets(accessToken, param.getUserId());
String sign = afterSign(verifyResult, nonceTickets);
String encodeUrl = URLEncoder.encode(redirectUrl, "UTF-8");
String url = "https://" + verifyResult.getOptimalDomain() + "/api/web/login?webankAppId=" + appId
+ "&version=" + version + "&nonce=" + nonce + "&orderNo=" + verifyResult.getOrderNo() +
"&faceId=" + verifyResult.getFaceId() + "&url=" + encodeUrl + "&from=browser&userId=" + param.getUserId()
+ "&sign=" + sign + "&redirectType=1";
return url;
}
/**
* 获取 Access Token
* 参考文档:https://cloud.tencent.com/document/product/1007/37304
* @return
*/
public String getAccessToken() {
Map<String, String> map = new HashMap<>();
map.put("app_id", appId);
map.put("secret", appSecret);
map.put("grant_type", "client_credential");
map.put("version", version);
String result = HttpClientUtil.doGet("https://miniprogram-kyc.tencentcloudapi.com/api/oauth2/access_token", map);
Map<String, Object> resultMap = JSONObject.parseObject(result, Map.class);
if (resultMap.get("code").equals("0")) {
Object access_token = resultMap.get("access_token");
return access_token.toString();
}
return null;
}
/**
* 获取 SIGN ticket
* 参考文档:https://cloud.tencent.com/document/product/1007/37305
* @param accessToken
* @return
*/
public String getSignTickets(String accessToken) {
Map<String, String> map = new HashMap<>();
map.put("app_id", appId);
map.put("access_token", accessToken);
map.put("type", "SIGN");
map.put("version", version);
String result = HttpClientUtil.doGet("https://miniprogram-kyc.tencentcloudapi.com/api/oauth2/api_ticket", map);
Map<String, Object> resultMap = JSONObject.parseObject(result, Map.class);
if (resultMap.get("code").equals("0")) {
List<Map> tickets = JSONObject.parseArray(resultMap.get("tickets").toString(), Map.class);
Object ticket = tickets.get(0).get("value");
return ticket.toString();
}
return null;
}
/**
* 获取 NONCE ticket
* 参考文档:https://cloud.tencent.com/document/product/1007/37306
* @param accessToken
* @param userId
* @return
*/
public String getNonceTickets(String accessToken, String userId) {
Map<String, String> map = new HashMap<>();
map.put("app_id", appId);
map.put("access_token", accessToken);
map.put("type", "NONCE");
map.put("version", version);
map.put("user_id", userId);
String result = HttpClientUtil.doGet("https://miniprogram-kyc.tencentcloudapi.com/api/oauth2/api_ticket", map);
Map<String, Object> resultMap = JSONObject.parseObject(result, Map.class);
if (resultMap.get("code").equals("0")) {
List<Map> tickets = JSONObject.parseArray(resultMap.get("tickets").toString(), Map.class);
Object ticket = tickets.get(0).get("value");
return ticket.toString();
}
return null;
}
/**
* 启动 H5 人脸核身所需签名
* @param verifyResult
* @param ticket
* @return
*/
public String afterSign(TencentFaceVerifyResult verifyResult, String ticket) {
List<String> values = new ArrayList<>();
values.add(version);
values.add(verifyResult.getOrderNo());
values.add(appId);
values.add(verifyResult.getFaceId());
values.add(nonce);
values.add(verifyResult.getUserId());
return sign(values, ticket);
}
/**
* 后台上送身份信息所需签名
* @param userId
* @param ticket
* @return
*/
public String beforeSign(String userId, String ticket) {
List<String> values = new ArrayList<>();
values.add(version);
values.add(appId);
values.add(nonce);
values.add(userId);
return sign(values, ticket);
}
/**
* 获取签名 参考文档 https://cloud.tencent.com/document/product/1007/57640
* @param values
* @param ticket
* @return
*/
public static String sign(List<String> values, String ticket) {
if (values == null) {
throw new NullPointerException("values is null");
}
values.removeAll(Collections.singleton(null));
values.add(ticket);
java.util.Collections.sort(values);
StringBuilder sb = new StringBuilder();
for (String s : values) {
sb.append(s);
}
return Hashing.sha1().hashString(sb,
Charsets.UTF_8).toString().toUpperCase();
}
}
在获取启动 H5 人脸核身地址时,将地址返回给前端,让他直接调就行。或许自己可以直接在浏览器打开那个地址,如果成功的话,会调到腾讯云的人脸核身页面。
这里用来比对的原图片是网络图片,如果是本地图片的话,直接将本地图片base64后encode就行。另外,回调的地址也要encode一下。不然会有40010错误码出现。