天天看点

技术使用总结-旷视人脸识别-(APP中H5接入)0.文档1.application2.dto3.constant4.service

文章目录

  • 0.文档
  • 1.application
  • 2.dto
  • 3.constant
  • 4.service

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;

    /**
     * idcard_mode = 0 时,idcard_name idcard_number 这两个参数必须传;在其他情况下可以不传,即使传递了也不会使用。
     */
    @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";

    /**
     * 表示目前 FaceID Lite 的使用状态
     */
    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, "服务器内部错误,请及时联系运营人员");
    }

    /**
     * 获取FaceID Lite 的使用状态
     * @param status
     * @return
     */
    public static String getStatusDesc(String status) {
        return StringUtils.isNotEmpty(ERROR_STATUS_MAP.get(status)) ? ERROR_STATUS_MAP.get(status) : OK;
    }

    /**
     * 获取人脸比对结果错误信息
     * @param errorMessage
     * @return
     */
    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;

/**
 * @PackageName: com.raven.springboot.service.faceid.megvii
 * @ClassName: FaceRecognitionService
 * @Blame: raven
 * @Date: 2021-08-02 15:35
 * @Description:
 */
@Service
@Slf4j
public class FaceRecognitionService {

    @Autowired
    private MegviiProperties megviiProperties;

    public MsgResult getToken(String bizNo, String idcardNumber, String idcardName) {

        // 构建请求参数
        H5GetTokenDTO getTokenDTO = builderH5GetTokenDTO(bizNo, idcardNumber, idcardName);

        // 调用获取tokenUrl
        String responseJson =  requestMegviiGetTokenUrl(getTokenDTO);

        // 返回调用结果
        return pareseGetTokenResponseJson(responseJson);
    }

    public MsgResult getResult(String bizNo){
        // 构建请求参数
        H5GetResultDTO getResultDTO = builderH5GetResultDTO(bizNo);

        // 调用获取tokenUrl
        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())
                // “1”表示KYC验证,表示最终的用户自拍照片将于参考照片比对
                .comparison_type("1")
                // 0:不拍摄身份证,而是通过 idcardName / idcardNumber 参数传入;
                .idcard_mode("0")
                .idcard_number(idcardNumber)
                .idcard_name(idcardName)
                .build();
    }
    /**
     * 校验人脸识别信息
     *
     * @param dataJson
     * @return
     */
    public MsgResult checkReturnDataInfo(String dataJson) {

        if (StringUtils.isEmpty(dataJson)) {
            log.error("Megvii | 旷视人脸识别消息接收错误,dataJson = {}", dataJson);
            return MsgResult.error("人脸识别错误!");
        }

        JSONObject dataJsonObject = JSONObject.parseObject(dataJson);

        // 检验FaceID Lite 的使用状态
        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();
    }

    /**
     * 校验是否为同一人
     *
     * @param verifyResult
     * @param requestId
     * @return
     */
    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;
    }

}