0.文档
[官方文档](https://faceid.com/pages/documents)
1.application
server:
port: 80
megvii:
appKey: XXX
secret: XXX
returnUrl: https://www.pilipili.com/
notifyUrl: https://www.pilipili.com/
webTitle: 代码生涯
2.dto
@Data
@Builder
public class H5GetResultDTO {
@ApiModelProperty(value = "调用此API的api_key", required = true)
private String api_key;
@ApiModelProperty(value = "调用此API的api_key的secret", required = true)
private String api_secret;
@ApiModelProperty(value = "通过 get_token, notify_url 或者 return_url 返回的活体业务编号。",required = true)
private String biz_id;
@ApiModelProperty(value = "是否返回活体图像数据:\n" +
"0(默认):不需要图像\n" +
"1:需要返回最佳活体质量图(image_best,当procedure_type为\"video\",\"still\"或\"flash\"时有效)\n" +
"2:需要返回身份证人像面图像\n" +
"3:需要返回身份证国徽面图像\n" +
"4:需要返回所有图像\n" +
"5:需要返回正脸自拍照片(仅当procedure_type为selfie时有效)\n" +
"6:需要返回侧脸自拍照片(仅当procedure_type为selfie时有效)", required = true)
private String return_image;
}
@Data
@Builder
public class H5GetTokenDTO {
@ApiModelProperty("调用此API的api_key")
private String api_key;
@ApiModelProperty("调用此API的api_key的secret")
private String api_secret;
@ApiModelProperty("用户完成或取消验证后网页跳转的目标URL")
private String return_url;
@ApiModelProperty("用户完成验证、取消验证、或验证超时后,由FaceID服务器请求客户服务器的URL。")
private String notify_url;
@ApiModelProperty("客户业务流水号,该号必须唯一。并会在notify和return时原封不动的返回给您的服务器,以帮助您确认每一笔业务的归属。")
private String biz_no;
@ApiModelProperty("验证网页展示用的标题文字,此参数默认为空,此时系统将采用默认的标题。")
private String web_title;
@ApiModelProperty("设定本次服务的类型,目前支持的比对类型为“KYC验证”(取值“1”)或“人脸比对”(取值“0”)。传递其他值调用将识别,返回错误码400(BAD_ARGUMENTS)。\n" +
"“1”表示KYC验证,表示最终的用户自拍照片将于参考照片比对。\n" +
"“0”表示人脸比对,FaceID将使用用户自己提供的照片(参数image_ref[x])作为比对人脸照。\n" +
"请注意:\n" +
"本参数影响验证流程中是否存在身份证拍摄环节:如果为“1”,则可选择包含身份证拍摄;如果为“0”,验证流程中将没有身份证拍摄。\n" +
"本参数取什么值将决定下面“二选一”参数组使用哪一组参数。")
private String comparison_type;
@ApiModelProperty("传递参数“0”,“1”,“2”,“3”或“4”,表示获取用户身份证信息的方法。传递其他值调用将识别,返回错误码400(BAD_ARGUMENTS)。\n" +
"0:不拍摄身份证,而是通过 idcard_name / idcard_number 参数传入;\n" +
"1:仅拍摄身份证人像面,可获取人像面所有信息;\n" +
"2:拍摄身份证人像面和身份证国徽面,可获取身份证所有信息;\n" +
"3:不拍摄身份证,但会要求用户在界面上输入身份证号和姓名;\n" +
"4:拍摄身份证人像面或用户输入身份证号和姓名,用户可在界面上自行选择身份证录入方法。注意:该参数只有在控制台中选择使用“浅色主题”时才生效,若未应用浅色主题而传入4,则返回错误码400(BAD_ARGUMENTS)。")
private String idcard_mode;
@ApiModelProperty("idcard_name, 需要KYC验证对象的姓名,使用UTF-8编码;")
private String idcard_name;
@ApiModelProperty("idcard_number, 需要KYC验证对象的身份证号,也就是一个18位长度的字符串。")
private String idcard_number;
}
@Data
@ConfigurationProperties(prefix = "megvii")
@Component
public class MegviiProperties {
private String secret;
private String appKey;
private String returnUrl;
private String notifyUrl;
private String webTitle;
}
3.constant
@Data
public class MsgResult<T> implements Serializable {
public static final String CODE_SUCCESS = "0";
public static final String MESSAGE_SUCCESS = "success";
@ApiModelProperty("状态码,0成功非0失败")
private String code;
@ApiModelProperty("状态码描述,0success非0失败原因")
private String message;
@ApiModelProperty("返回数据")
private T data;
private MsgResult(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static MsgResult success() {
return new MsgResult(CODE_SUCCESS, MESSAGE_SUCCESS, null);
}
public static <T> MsgResult success(T data) {
return new MsgResult(CODE_SUCCESS, MESSAGE_SUCCESS, data);
}
public static <T> MsgResult success(T data, String message) {
return new MsgResult(CODE_SUCCESS, message, data);
}
public static MsgResult error(ErrorCode errorCode, String appendMessage) {
String message = String.format(errorCode.getMessage(), appendMessage == null ? "" : appendMessage);
return new MsgResult(errorCode.getCode(), message, null);
}
public static MsgResult error(ErrorCode errorCode) {
return new MsgResult(errorCode.getCode(), errorCode.getMessage(), null);
}
public static MsgResult error(String code, String message) {
return new MsgResult(code, message, null);
}
public static MsgResult error(String message) {
return new MsgResult(ErrorCode.OPER_FAIL.getCode(), message, null);
}
public boolean hasSuccess() {
return CODE_SUCCESS.equals(this.code);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("MsgResult{");
sb.append("code='").append(this.code).append('\'');
sb.append(", message='").append(this.message).append('\'');
sb.append(", data=").append(this.data);
sb.append('}');
return sb.toString();
}
public String toSimpleString() {
StringBuilder sb = new StringBuilder("MsgResult{");
sb.append("code='").append(this.code).append('\'');
sb.append(", message='").append(this.message).append('\'');
sb.append('}');
return sb.toString();
}
public String getCode() {
return this.code;
}
public String getMessage() {
return this.message;
}
public T getData() {
return this.data;
}
}
public class ErrorCode implements Serializable {
public static final ErrorCode PARAM_REQUIRED = new ErrorCode("4001", "参数:%s不能为空");
public static final ErrorCode PARAM_INVALID = new ErrorCode("4002", "参数:%s格式无效");
public static final ErrorCode SIGN = new ErrorCode("4003", "参数签名验证失败");
public static final ErrorCode AUTHORITY = new ErrorCode("4004", "您没有权限访问该接口");
public static final ErrorCode DECRYPT = new ErrorCode("4005", "请求数据解密失败");
public static final ErrorCode INVALID_CALLER = new ErrorCode("4006", "无效的调用方");
public static final ErrorCode UPLOAD_TYPE = new ErrorCode("4007", "您上传的文件类型不允许");
public static final ErrorCode UPLOAD_SIZE = new ErrorCode("4008", "您上传的文件大小不能超过[%s]KB");
public static final ErrorCode SYSTEM_BUSY = new ErrorCode("4009", "服务器繁忙,请稍后重试");
public static final ErrorCode NEED_LOGIN = new ErrorCode("4010", "登录超时,请重新登录");
public static final ErrorCode OPER_FAIL = new ErrorCode("4011", "操作失败:%s");
public static final ErrorCode SERVER = new ErrorCode("5000", "服务器内部错误:%s");
public static final ErrorCode SERVER_REDIS = new ErrorCode("5001", "服务器内部错误:%s");
public static final ErrorCode SERVER_MYSQL = new ErrorCode("5002", "服务器内部错误:%s");
public static final ErrorCode SERVER_ES = new ErrorCode("5003", "服务器内部错误:%s");
public static final ErrorCode SERVER_HTTP = new ErrorCode("5004", "服务器内部错误:%s");
public static final ErrorCode SERVER_CASSANDRA = new ErrorCode("5005", "服务器内部错误:%s");
public static final ErrorCode SERVER_ORACLE = new ErrorCode("5006", "服务器内部错误:%s");
private String code;
private String message;
public ErrorCode(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return this.code;
}
public String getMessage() {
return this.message;
}
}
public class MegviiConstant {
public static final String GET_TOKEN_URL = "https://api.megvii.com/faceid/lite/get_token";
public static final String GET_RESULT__URL = "https://api.megvii.com/faceid/lite/get_result";
public static final String ERROR_MESSAGE = "error_message";
public static final String OK = "OK";
private static final String ERROR_STATUS_NOT_STARTED = "NOT_STARTED";
private static final String ERROR_STATUS_PROCESSING = "PROCESSING";
private static final String ERROR_STATUS_WEBRTC_UNSUPPORTED = "WEBRTC_UNSUPPORTED";
private static final String ERROR_STATUS_FAILED = "FAILED";
private static final String ERROR_STATUS_CANCELLED = "CANCELLED";
private static final String ERROR_STATUS_TIMEOUT = "TIMEOUT";
private static final String VERIFY_ERROR_MESSAGE_NO_SUCH_ID_NUMBER = "NO_SUCH_ID_NUMBER";
private static final String VERIFY_ERROR_MESSAGE_ID_NUMBER_NAME_NOT_MATCH = "ID_NUMBER_NAME_NOT_MATCH";
private static final String VERIFY_ERROR_MESSAGE_IMAGE_ERROR_UNSUPPORTED_FORMAT = "IMAGE_ERROR_UNSUPPORTED_FORMAT";
private static final String VERIFY_ERROR_MESSAGE_NO_FACE_FOUND = "NO_FACE_FOUND";
private static final String VERIFY_ERROR_MESSAGE_DATA_SOURCE_ERROR = "DATA_SOURCE_ERROR";
private static final String VERIFY_ERROR_MESSAGE_INTERNAL_ERROR = "INTERNAL_ERROR";
public static final String LIVENESS_RESULT_PASS = "PASS";
private static Map<String, String> ERROR_STATUS_MAP = Maps.newHashMap();
private static Map<String, String> VERIFY_ERROR_MESSAGE__MAP = Maps.newHashMap();
static {
ERROR_STATUS_MAP.put(ERROR_STATUS_NOT_STARTED, "还没有开始验证");
ERROR_STATUS_MAP.put(ERROR_STATUS_PROCESSING, "正在进行 FaceID Lite 验证");
ERROR_STATUS_MAP.put(ERROR_STATUS_WEBRTC_UNSUPPORTED, "表示浏览器不支持引起失败");
ERROR_STATUS_MAP.put(ERROR_STATUS_FAILED, "验证流程未完成或出现异常");
ERROR_STATUS_MAP.put(ERROR_STATUS_CANCELLED, "用户主动取消了验证流程");
ERROR_STATUS_MAP.put(ERROR_STATUS_TIMEOUT, "流程超时");
VERIFY_ERROR_MESSAGE__MAP.put(VERIFY_ERROR_MESSAGE_NO_SUCH_ID_NUMBER, "没有此身份证号码的记录");
VERIFY_ERROR_MESSAGE__MAP.put(VERIFY_ERROR_MESSAGE_ID_NUMBER_NAME_NOT_MATCH, "身份证号码与提供的姓名不匹配");
VERIFY_ERROR_MESSAGE__MAP.put(VERIFY_ERROR_MESSAGE_IMAGE_ERROR_UNSUPPORTED_FORMAT, "姓名和身份证号正确,但图片无法解析或者没有可比对图片");
VERIFY_ERROR_MESSAGE__MAP.put(VERIFY_ERROR_MESSAGE_NO_FACE_FOUND, "对应的图像没有检测到人脸");
VERIFY_ERROR_MESSAGE__MAP.put(VERIFY_ERROR_MESSAGE_DATA_SOURCE_ERROR, "调用比对数据发生错误");
VERIFY_ERROR_MESSAGE__MAP.put(VERIFY_ERROR_MESSAGE_INTERNAL_ERROR, "服务器内部错误,请及时联系运营人员");
}
public static String getStatusDesc(String status) {
return StringUtils.isNotEmpty(ERROR_STATUS_MAP.get(status)) ? ERROR_STATUS_MAP.get(status) : OK;
}
public static String getVerifyResultErrorMessage(String errorMessage) {
return StringUtils.isNotEmpty(VERIFY_ERROR_MESSAGE__MAP.get(errorMessage)) ? VERIFY_ERROR_MESSAGE__MAP.get(errorMessage) : OK;
}
}
4.service
package com.raven.springboot.faceid.megvii.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.raven.springboot.common.constant.MegviiConstant;
import com.raven.springboot.common.constant.MsgResult;
import com.raven.springboot.common.properties.MegviiProperties;
import com.raven.springboot.common.utils.HttpClientUtil;
import com.raven.springboot.faceid.megvii.dto.H5GetResultDTO;
import com.raven.springboot.faceid.megvii.dto.H5GetTokenDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Objects;
@Service
@Slf4j
public class FaceRecognitionService {
@Autowired
private MegviiProperties megviiProperties;
public MsgResult getToken(String bizNo, String idcardNumber, String idcardName) {
H5GetTokenDTO getTokenDTO = builderH5GetTokenDTO(bizNo, idcardNumber, idcardName);
String responseJson = requestMegviiGetTokenUrl(getTokenDTO);
return pareseGetTokenResponseJson(responseJson);
}
public MsgResult getResult(String bizNo){
H5GetResultDTO getResultDTO = builderH5GetResultDTO(bizNo);
String responseJson = requestMegviiGetResultUrl(getResultDTO);
return paresGetResultResponseJson(responseJson);
}
private MsgResult paresGetResultResponseJson(String responseJson) {
JSONObject jsonObject = JSONObject.parseObject(responseJson);
String errorMessage = jsonObject.getString(MegviiConstant.ERROR_MESSAGE);
if (StringUtils.isNotBlank(errorMessage)){
log.info("Megvii | 获取result请求失败,失败原因 {}",errorMessage);
return MsgResult.error("获取result请求失败");
}
MsgResult msgResult = this.checkReturnDataInfo(JSON.toJSONString(jsonObject));
if (!msgResult.hasSuccess()){
return msgResult;
}
return MsgResult.success(jsonObject);
}
private String requestMegviiGetResultUrl(H5GetResultDTO getResultDTO) {
Map<String,String> map = JSON.parseObject(JSON.toJSONString(getResultDTO), Map.class);
return HttpClientUtil.doGet(MegviiConstant.GET_RESULT__URL, map);
}
private H5GetResultDTO builderH5GetResultDTO(String bizNo) {
return H5GetResultDTO.builder()
.api_key(megviiProperties.getAppKey())
.api_secret(megviiProperties.getSecret())
.biz_id(bizNo)
.return_image("4")
.build();
}
private MsgResult pareseGetTokenResponseJson(String responseJson) {
JSONObject jsonObject = JSONObject.parseObject(responseJson);
String errorMessage = jsonObject.getString(MegviiConstant.ERROR_MESSAGE);
if (StringUtils.isNotBlank(errorMessage)){
log.info("Megvii | 获取token请求失败,失败原因 {}",errorMessage);
return MsgResult.error("获取token请求失败");
}
return MsgResult.success(jsonObject);
}
private String requestMegviiGetTokenUrl(H5GetTokenDTO getTokenDTO) {
Map<String,String> map = JSON.parseObject(JSON.toJSONString(getTokenDTO), Map.class);
return HttpClientUtil.doPost(MegviiConstant.GET_TOKEN_URL, map);
}
private H5GetTokenDTO builderH5GetTokenDTO(String bizNo, String idcardNumber, String idcardName) {
return H5GetTokenDTO.builder()
.api_key(megviiProperties.getAppKey())
.api_secret(megviiProperties.getSecret())
.return_url(megviiProperties.getReturnUrl())
.notify_url(megviiProperties.getNotifyUrl())
.biz_no(bizNo)
.web_title(megviiProperties.getWebTitle())
.comparison_type("1")
.idcard_mode("0")
.idcard_number(idcardNumber)
.idcard_name(idcardName)
.build();
}
public MsgResult checkReturnDataInfo(String dataJson) {
if (StringUtils.isEmpty(dataJson)) {
log.error("Megvii | 旷视人脸识别消息接收错误,dataJson = {}", dataJson);
return MsgResult.error("人脸识别错误!");
}
JSONObject dataJsonObject = JSONObject.parseObject(dataJson);
String requestId = dataJsonObject.getString("request_id");
String status = dataJsonObject.getString("status");
String statusDesc = MegviiConstant.getStatusDesc(status);
if (!StringUtils.equals(MegviiConstant.OK, statusDesc)) {
log.error("Megvii | 旷视人脸识别消息接收错误, request_id:{}, 错误原因:{}", requestId, statusDesc);
return MsgResult.error("人脸识别错误!");
}
JSONObject livenessResult = dataJsonObject.getJSONObject("liveness_result");
String result = livenessResult.getString("result");
if (!StringUtils.equals(MegviiConstant.LIVENESS_RESULT_PASS, result)) {
log.error("Megvii | 旷视人脸识别消息接收错误, 活体检测失败, request_id:{}", requestId);
return MsgResult.error("人脸识别错误!");
}
JSONObject verifyResult = dataJsonObject.getJSONObject("verify_result");
if (this.verifyResultRisk(verifyResult, requestId)) {
return MsgResult.error("人脸识别错误!");
}
return MsgResult.success();
}
private boolean verifyResultRisk(JSONObject verifyResult, String requestId) {
if (Objects.isNull(verifyResult)) {
return true;
}
String verifyResultErrorMessage = MegviiConstant.getVerifyResultErrorMessage(MegviiConstant.ERROR_MESSAGE);
if (!StringUtils.equals(MegviiConstant.OK, verifyResultErrorMessage)) {
log.error("Megvii | 旷视人脸识别消息接收错误, 在做人脸比对的时候出现错误, request_id:{}, 错误原因:{}", requestId, verifyResult.getString(MegviiConstant.ERROR_MESSAGE));
return true;
}
JSONObject resultFaceid = verifyResult.getJSONObject("result_faceid");
Double confidence = resultFaceid.getDouble("confidence");
JSONObject thresholds = resultFaceid.getJSONObject("thresholds");
Double score = thresholds.getDouble("1e-4");
if (confidence < score) {
log.error("Megvii | 旷视人脸识别消息接收错误, 系统认证不是同一个人, request_id:{}", requestId);
return true;
}
return false;
}
}