Browse Source

examcloud-face-verify-starter

deason 2 years ago
parent
commit
b5c9bb147c
23 changed files with 1586 additions and 0 deletions
  1. 66 0
      examcloud-starters/examcloud-face-verify-starter/pom.xml
  2. 31 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/FaceVerifyAutoConfiguration.java
  3. 109 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/FaceVerifyProperties.java
  4. 22 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/exception/ApiLimitException.java
  5. 26 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/Constants.java
  6. 11 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/IModel.java
  7. 46 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/IResult.java
  8. 73 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduAccessInfo.java
  9. 31 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduAccessToken.java
  10. 33 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduFaceInfo.java
  11. 120 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduResponseInfo.java
  12. 61 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduResultInfo.java
  13. 56 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduThresholds.java
  14. 23 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/faceplus/FacePlusFaceInfo.java
  15. 155 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/faceplus/FacePlusResponseInfo.java
  16. 56 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/faceplus/FacePlusThresholds.java
  17. 97 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/service/FaceVerifyService.java
  18. 357 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/service/impl/FaceVerifyServiceImpl.java
  19. 91 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/utils/HttpClientBuilder.java
  20. 69 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/utils/JsonHelper.java
  21. 50 0
      examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/utils/StrUtils.java
  22. 2 0
      examcloud-starters/examcloud-face-verify-starter/src/main/resources/META-INF/spring.factories
  23. 1 0
      examcloud-starters/pom.xml

+ 66 - 0
examcloud-starters/examcloud-face-verify-starter/pom.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>examcloud-face-verify-starter</artifactId>
+    <packaging>jar</packaging>
+
+    <parent>
+        <groupId>cn.com.qmth.examcloud.starters</groupId>
+        <artifactId>examcloud-starters</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-log4j2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>${basedir}/src/main/resources</directory>
+                <includes>
+                    <include>**/*.*</include>
+                </includes>
+            </resource>
+        </resources>
+    </build>
+
+</project>

+ 31 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/FaceVerifyAutoConfiguration.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.examcloud.starters.face.verify;
+
+import cn.com.qmth.examcloud.starters.face.verify.service.FaceVerifyService;
+import cn.com.qmth.examcloud.starters.face.verify.service.impl.FaceVerifyServiceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+@Configuration
+@EnableConfigurationProperties(FaceVerifyProperties.class)
+@ConditionalOnProperty(prefix = "examcloud.starters.face.verify", name = "active", havingValue = "true")
+public class FaceVerifyAutoConfiguration {
+
+    private final static Logger log = LoggerFactory.getLogger(FaceVerifyAutoConfiguration.class);
+
+    @Bean
+    public FaceVerifyService faceVerifyService(FaceVerifyProperties faceVerifyProperties) {
+        log.info("faceVerifyService init...");
+        FaceVerifyServiceImpl impl = new FaceVerifyServiceImpl();
+        impl.setProperties(faceVerifyProperties);
+        return impl;
+    }
+
+}

+ 109 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/FaceVerifyProperties.java

@@ -0,0 +1,109 @@
+package cn.com.qmth.examcloud.starters.face.verify;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.io.Serializable;
+
+/**
+ * 人脸识别相关API配置信息
+ *
+ * @author: QMTH
+ * @since: 2019/03/25
+ */
+@ConfigurationProperties(prefix = "examcloud.starters.face.verify", ignoreUnknownFields = false)
+public class FaceVerifyProperties implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /*
+     * 是否启用
+     */
+    private Boolean active;
+
+    /*
+     * Face++ API Key
+     */
+    private String facePlusKey;
+
+    /*
+     * Face++ API Secret
+     */
+    private String facePlusSecret;
+
+    /*
+     * 百度 API Key
+     */
+    private String baiduKey;
+
+    /*
+     * 百度 API Secret
+     */
+    private String baiduSecret;
+
+    /**
+     * 短信预警手机号(多个逗号分隔)
+     */
+    private String noticePhones;
+
+    /**
+     * 预警短信模板
+     */
+    private String smsTemplate;
+
+    public Boolean getActive() {
+        return active;
+    }
+
+    public void setActive(Boolean active) {
+        this.active = active;
+    }
+
+    public String getFacePlusKey() {
+        return facePlusKey;
+    }
+
+    public void setFacePlusKey(String facePlusKey) {
+        this.facePlusKey = facePlusKey;
+    }
+
+    public String getFacePlusSecret() {
+        return facePlusSecret;
+    }
+
+    public void setFacePlusSecret(String facePlusSecret) {
+        this.facePlusSecret = facePlusSecret;
+    }
+
+    public String getBaiduKey() {
+        return baiduKey;
+    }
+
+    public void setBaiduKey(String baiduKey) {
+        this.baiduKey = baiduKey;
+    }
+
+    public String getBaiduSecret() {
+        return baiduSecret;
+    }
+
+    public void setBaiduSecret(String baiduSecret) {
+        this.baiduSecret = baiduSecret;
+    }
+
+    public String getNoticePhones() {
+        return noticePhones;
+    }
+
+    public void setNoticePhones(String noticePhones) {
+        this.noticePhones = noticePhones;
+    }
+
+    public String getSmsTemplate() {
+        return smsTemplate;
+    }
+
+    public void setSmsTemplate(String smsTemplate) {
+        this.smsTemplate = smsTemplate;
+    }
+
+}

+ 22 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/exception/ApiLimitException.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.starters.face.verify.exception;
+
+public class ApiLimitException extends RuntimeException {
+
+    private String desc;
+
+    public ApiLimitException(String desc) {
+        super(desc);
+        this.desc = desc;
+    }
+
+    public ApiLimitException(String desc, Throwable e) {
+        super(desc, e);
+        this.desc = desc;
+    }
+
+    @Override
+    public String toString() {
+        return desc;
+    }
+
+}

+ 26 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/Constants.java

@@ -0,0 +1,26 @@
+package cn.com.qmth.examcloud.starters.face.verify.model;
+
+/**
+ * 人脸照片处理接口地址前缀常量定义(待移至配置文件内)
+ *
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public interface Constants {
+
+    /* 百度 获取Token API */
+    String BAI_DU_TOKEN_URL = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s";
+
+    /* 百度 人脸活体检测API */
+    String BAI_DU_DETECT_FACE_URL = "https://aip.baidubce.com/rest/2.0/face/v3/faceverify?access_token=%s";
+
+    /* 百度 人脸检测API(可检测人脸数) */
+    String BAIDU_DETECT_FACE_WITH_FACE_SIZE_URL = "https://aip.baidubce.com/rest/2.0/face/v3/detect?access_token=%s";
+
+    /* Face++ 人脸识别对比API */
+    String FACE_PLUS_COMPARE_FACE_URL = "https://api-cn.faceplusplus.com/facepp/v3/compare";
+
+    /* Face++ 人脸的活体检测API(可检测人脸数) */
+    String FACE_PLUS_DETECT_FACE_URL = "https://api-cn.faceplusplus.com/facepp/v3/detect";
+
+}

+ 11 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/IModel.java

@@ -0,0 +1,11 @@
+package cn.com.qmth.examcloud.starters.face.verify.model;
+
+import java.io.Serializable;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public interface IModel extends Serializable {
+
+}

+ 46 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/IResult.java

@@ -0,0 +1,46 @@
+package cn.com.qmth.examcloud.starters.face.verify.model;
+
+import java.io.Serializable;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public interface IResult extends Serializable {
+
+    /**
+     * 是否人脸活体检测通过
+     */
+    boolean isDetectPass();
+
+    /**
+     * 是否人脸识别对比通过
+     */
+    boolean isComparePass();
+
+    /**
+     * 置信度
+     */
+    Double getConfidenceValue();
+
+    /**
+     * 置信度阈值(JSON)
+     */
+    String getThresholdValue();
+
+    /**
+     * 人脸数
+     */
+    int getFaceSize();
+
+    /**
+     * 请求并发是否超限
+     */
+    boolean requestOutOfLimit();
+
+    /**
+     * Token是否过期
+     */
+    boolean isTokenExpired();
+
+}

+ 73 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduAccessInfo.java

@@ -0,0 +1,73 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.baidu;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IModel;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class BaiduAccessInfo implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    private String access_token;
+
+    private String refresh_token;
+
+    private String session_key;
+
+    private String session_secret;
+
+    private String scope;
+
+    private Long expires_in;
+
+    public String getAccess_token() {
+        return access_token;
+    }
+
+    public void setAccess_token(String access_token) {
+        this.access_token = access_token;
+    }
+
+    public String getRefresh_token() {
+        return refresh_token;
+    }
+
+    public void setRefresh_token(String refresh_token) {
+        this.refresh_token = refresh_token;
+    }
+
+    public String getSession_key() {
+        return session_key;
+    }
+
+    public void setSession_key(String session_key) {
+        this.session_key = session_key;
+    }
+
+    public String getSession_secret() {
+        return session_secret;
+    }
+
+    public void setSession_secret(String session_secret) {
+        this.session_secret = session_secret;
+    }
+
+    public String getScope() {
+        return scope;
+    }
+
+    public void setScope(String scope) {
+        this.scope = scope;
+    }
+
+    public Long getExpires_in() {
+        return expires_in;
+    }
+
+    public void setExpires_in(Long expires_in) {
+        this.expires_in = expires_in;
+    }
+
+}

+ 31 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduAccessToken.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.baidu;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IModel;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class BaiduAccessToken implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    private static String TOKEN = null;
+
+    public static String getValue() {
+        return TOKEN;
+    }
+
+    public static void setValue(String value) {
+        TOKEN = value;
+    }
+
+    public static boolean isNotEmpty() {
+        if (TOKEN != null && TOKEN.length() > 0) {
+            return true;
+        }
+
+        return false;
+    }
+
+}

+ 33 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduFaceInfo.java

@@ -0,0 +1,33 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.baidu;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IModel;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class BaiduFaceInfo implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    private String face_token;//人脸图片的唯一标识
+
+    private Double face_probability;//人脸置信度(代表这是一张人脸的概率,0最小、1最大)
+
+    public String getFace_token() {
+        return face_token;
+    }
+
+    public void setFace_token(String face_token) {
+        this.face_token = face_token;
+    }
+
+    public Double getFace_probability() {
+        return face_probability;
+    }
+
+    public void setFace_probability(Double face_probability) {
+        this.face_probability = face_probability;
+    }
+
+}

+ 120 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduResponseInfo.java

@@ -0,0 +1,120 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.baidu;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IResult;
+import cn.com.qmth.examcloud.starters.face.verify.utils.JsonHelper;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class BaiduResponseInfo implements IResult {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer error_code;
+
+    private String error_msg;
+
+    private Long log_id;
+
+    private BaiduResultInfo result;
+
+    @Override
+    public boolean isDetectPass() {
+        //是否人脸活体检测通过 (默认推荐值:0.393241)
+        if (getError_code() == 0 && result != null && result.getFace_liveness() > 0.39) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isComparePass() {
+        //是否人脸识别对比通过(暂未实现)
+        return false;
+    }
+
+    @Override
+    public Double getConfidenceValue() {
+        //置信度
+        if (result != null) {
+            return result.getFace_liveness();
+        }
+        return null;
+    }
+
+    @Override
+    public String getThresholdValue() {
+        //置信度阈值(JSON)
+        if (result != null && result.getThresholds() != null) {
+            return new JsonHelper().toJson(result.getThresholds());
+        }
+        return null;
+    }
+
+    @Override
+    public int getFaceSize() {
+        //人脸数
+        if (getError_code() == 0 && result != null) {
+            return result.getFace_num();
+        }
+        return 0;
+    }
+
+    @Override
+    public boolean requestOutOfLimit() {
+        //请求并发是否超限(默认12个/秒)
+        if (getError_code() == 18) {
+            return true;//百度错误码为18
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isTokenExpired() {
+        // Token默认有效期为30天
+        if (getError_code() == 110 || getError_code() == 111) {
+            // 已失效或过期
+            return true;
+        }
+
+        // Token有效
+        return false;
+    }
+
+    public Integer getError_code() {
+        if (error_code == null) {
+            return -1;
+        }
+        return error_code;
+    }
+
+    public void setError_code(Integer error_code) {
+        this.error_code = error_code;
+    }
+
+    public String getError_msg() {
+        return error_msg;
+    }
+
+    public void setError_msg(String error_msg) {
+        this.error_msg = error_msg;
+    }
+
+    public Long getLog_id() {
+        return log_id;
+    }
+
+    public void setLog_id(Long log_id) {
+        this.log_id = log_id;
+    }
+
+    public BaiduResultInfo getResult() {
+        return result;
+    }
+
+    public void setResult(BaiduResultInfo result) {
+        this.result = result;
+    }
+
+}

+ 61 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduResultInfo.java

@@ -0,0 +1,61 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.baidu;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IModel;
+
+import java.util.List;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class BaiduResultInfo implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    private Double face_liveness;//活体分数值
+
+    private Integer face_num;//人脸数
+
+    private List<BaiduFaceInfo> face_list;//人脸信息列表
+
+    private BaiduThresholds thresholds;//置信度阈值(误识率越低,准确率越高)
+
+    public Double getFace_liveness() {
+        if (face_liveness == null) {
+            return 0D;
+        }
+        return face_liveness;
+    }
+
+    public Integer getFace_num() {
+        if (face_num == null) {
+            return 0;
+        }
+        return face_num;
+    }
+
+    public List<BaiduFaceInfo> getFace_list() {
+        return face_list;
+    }
+
+    public BaiduThresholds getThresholds() {
+        return thresholds;
+    }
+
+    public void setFace_liveness(Double face_liveness) {
+        this.face_liveness = face_liveness;
+    }
+
+    public void setFace_num(Integer face_num) {
+        this.face_num = face_num;
+    }
+
+    public void setFace_list(List<BaiduFaceInfo> face_list) {
+        this.face_list = face_list;
+    }
+
+    public void setThresholds(BaiduThresholds thresholds) {
+        this.thresholds = thresholds;
+    }
+
+}

+ 56 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/baidu/BaiduThresholds.java

@@ -0,0 +1,56 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.baidu;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IModel;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class BaiduThresholds implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonProperty("frr_1e-2")
+    private Float frr_1e_2;//百分之一误识率的阈值
+
+    @JsonProperty("frr_1e-3")
+    private Float frr_1e_3;//千分之一误识率的阈值
+
+    @JsonProperty("frr_1e-4")
+    private Float frr_1e_4;//万分之一误识率的阈值
+
+    public Float getFrr_1e_2() {
+        if (frr_1e_2 == null) {
+            return 0F;
+        }
+        return frr_1e_2;
+    }
+
+    public void setFrr_1e_2(Float frr_1e_2) {
+        this.frr_1e_2 = frr_1e_2;
+    }
+
+    public Float getFrr_1e_3() {
+        if (frr_1e_3 == null) {
+            return 0F;
+        }
+        return frr_1e_3;
+    }
+
+    public void setFrr_1e_3(Float frr_1e_3) {
+        this.frr_1e_3 = frr_1e_3;
+    }
+
+    public Float getFrr_1e_4() {
+        if (frr_1e_4 == null) {
+            return 0F;
+        }
+        return frr_1e_4;
+    }
+
+    public void setFrr_1e_4(Float frr_1e_4) {
+        this.frr_1e_4 = frr_1e_4;
+    }
+
+}

+ 23 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/faceplus/FacePlusFaceInfo.java

@@ -0,0 +1,23 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.faceplus;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IModel;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class FacePlusFaceInfo implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    private String face_token;
+
+    public String getFace_token() {
+        return face_token;
+    }
+
+    public void setFace_token(String face_token) {
+        this.face_token = face_token;
+    }
+
+}

+ 155 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/faceplus/FacePlusResponseInfo.java

@@ -0,0 +1,155 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.faceplus;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IResult;
+import cn.com.qmth.examcloud.starters.face.verify.utils.JsonHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class FacePlusResponseInfo implements IResult {
+
+    private static final long serialVersionUID = 1L;
+
+    private static Logger log = LoggerFactory.getLogger(FacePlusResponseInfo.class);
+
+    private String request_id;
+
+    private Float confidence;
+
+    private FacePlusThresholds thresholds;
+
+    private Integer time_used;
+
+    private String error_message;
+
+    private String image_id;
+
+    private List<FacePlusFaceInfo> faces;
+
+    @Override
+    public boolean isDetectPass() {
+        //是否人脸活体检测通过 (只有一张人脸算成功,否则算失败)
+        if (faces != null) {
+            log.info("faces size:" + faces.size());
+
+            if (faces.size() == 1) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean isComparePass() {
+        //是否人脸识别对比通过
+        if (confidence != null && thresholds != null && confidence >= thresholds.getLe_4()) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public Double getConfidenceValue() {
+        //置信度
+        if (confidence != null) {
+            return confidence.doubleValue();
+        }
+        return null;
+    }
+
+    @Override
+    public String getThresholdValue() {
+        //置信度阈值(JSON)
+        if (thresholds != null) {
+            return new JsonHelper().toJson(thresholds);
+        }
+        return null;
+    }
+
+    @Override
+    public int getFaceSize() {
+        //人脸数
+        if (faces == null) {
+            return 0;
+        }
+        return faces.size();
+    }
+
+    @Override
+    public boolean requestOutOfLimit() {
+        //请求并发是否超限(默认20个/秒)
+        if ("CONCURRENCY_LIMIT_EXCEEDED".equals(getError_message())) {
+            return true;//Face++错误码为CONCURRENCY_LIMIT_EXCEEDED
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean isTokenExpired() {
+        return false;//ignore
+    }
+
+    public String getRequest_id() {
+        return request_id;
+    }
+
+    public void setRequest_id(String request_id) {
+        this.request_id = request_id;
+    }
+
+    public Float getConfidence() {
+        return confidence;
+    }
+
+    public void setConfidence(Float confidence) {
+        this.confidence = confidence;
+    }
+
+    public FacePlusThresholds getThresholds() {
+        return thresholds;
+    }
+
+    public void setThresholds(FacePlusThresholds thresholds) {
+        this.thresholds = thresholds;
+    }
+
+    public Integer getTime_used() {
+        return time_used;
+    }
+
+    public void setTime_used(Integer time_used) {
+        this.time_used = time_used;
+    }
+
+    public String getError_message() {
+        return error_message;
+    }
+
+    public void setError_message(String error_message) {
+        this.error_message = error_message;
+    }
+
+    public String getImage_id() {
+        return image_id;
+    }
+
+    public void setImage_id(String image_id) {
+        this.image_id = image_id;
+    }
+
+    public List<FacePlusFaceInfo> getFaces() {
+        return faces;
+    }
+
+    public void setFaces(List<FacePlusFaceInfo> faces) {
+        this.faces = faces;
+    }
+
+}

+ 56 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/model/faceplus/FacePlusThresholds.java

@@ -0,0 +1,56 @@
+package cn.com.qmth.examcloud.starters.face.verify.model.faceplus;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IModel;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class FacePlusThresholds implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonProperty("1e-3")
+    private Float le_3;
+
+    @JsonProperty("1e-4")
+    private Float le_4;
+
+    @JsonProperty("1e-5")
+    private Float le_5;
+
+    public Float getLe_3() {
+        if (le_3 == null) {
+            return 0F;
+        }
+        return le_3;
+    }
+
+    public void setLe_3(Float le_3) {
+        this.le_3 = le_3;
+    }
+
+    public Float getLe_4() {
+        if (le_4 == null) {
+            return 0F;
+        }
+        return le_4;
+    }
+
+    public void setLe_4(Float le_4) {
+        this.le_4 = le_4;
+    }
+
+    public Float getLe_5() {
+        if (le_5 == null) {
+            return 0F;
+        }
+        return le_5;
+    }
+
+    public void setLe_5(Float le_5) {
+        this.le_5 = le_5;
+    }
+
+}

+ 97 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/service/FaceVerifyService.java

@@ -0,0 +1,97 @@
+package cn.com.qmth.examcloud.starters.face.verify.service;
+
+import cn.com.qmth.examcloud.starters.face.verify.model.IResult;
+
+import java.io.File;
+import java.io.Serializable;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public interface FaceVerifyService extends Serializable {
+
+    /**
+     * 执行百度的人脸的活体检测
+     *
+     * @param targetImageUrl 图片访问链接
+     * @return
+     */
+    IResult detectFaceByBaidu(String targetImageUrl);
+
+    /**
+     * 执行百度的人脸的活体检测
+     *
+     * @param imageFileBytes 图片文件流
+     * @return
+     */
+    IResult detectFaceByBaidu(byte[] imageFileBytes);
+
+    /**
+     * 执行百度的人脸的活体检测
+     *
+     * @param targetImageFile 图片文件
+     * @return
+     */
+    IResult detectFaceByBaidu(File targetImageFile);
+
+    /**
+     * 执行百度的人脸的检测(可检测人脸数)
+     *
+     * @param targetImageFile 图片文件
+     * @return
+     */
+    IResult detectFaceWithFaceSizeByBaidu(File targetImageFile);
+
+    /**
+     * 执行百度的人脸的检测(可检测人脸数)
+     *
+     * @param imageFileBytes 图片文件流
+     * @return
+     */
+    IResult detectFaceWithFaceSizeByBaidu(byte[] imageFileBytes);
+
+    /**
+     * 执行Face++的人脸的活体检测
+     *
+     * @param targetImageFile 图片文件
+     * @return
+     */
+    IResult detectFaceByFacePlus(File targetImageFile);
+
+    /**
+     * 执行Face++的人脸的活体检测
+     *
+     * @param imageFileBytes 文件流
+     * @param imageFileName  文件名
+     * @return
+     */
+    IResult detectFaceByFacePlus(byte[] imageFileBytes, String imageFileName);
+
+    /**
+     * 执行Face++的人脸识别对比
+     *
+     * @param sourceImageUrl 底照访问链接
+     * @param targetImageUrl 抓拍照访问链接
+     * @return
+     */
+    IResult compareFaceByFacePlus(String sourceImageUrl, String targetImageUrl);
+
+    /**
+     * 执行Face++的人脸识别对比
+     *
+     * @param sourceImageFile 底照文件
+     * @param targetImageFile 抓拍照文件
+     */
+    IResult compareFaceByFacePlus(File sourceImageFile, File targetImageFile);
+
+    /**
+     * 执行Face++的人脸识别对比
+     *
+     * @param sourceImageUrl  底照访问链接
+     * @param targetImageFile 抓拍照文件
+     * @return
+     */
+    IResult compareFaceByFacePlus(String sourceImageUrl, File targetImageFile);
+
+}

+ 357 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/service/impl/FaceVerifyServiceImpl.java

@@ -0,0 +1,357 @@
+package cn.com.qmth.examcloud.starters.face.verify.service.impl;
+
+import cn.com.qmth.examcloud.starters.face.verify.FaceVerifyProperties;
+import cn.com.qmth.examcloud.starters.face.verify.model.IResult;
+import cn.com.qmth.examcloud.starters.face.verify.model.baidu.BaiduAccessInfo;
+import cn.com.qmth.examcloud.starters.face.verify.model.baidu.BaiduAccessToken;
+import cn.com.qmth.examcloud.starters.face.verify.model.baidu.BaiduResponseInfo;
+import cn.com.qmth.examcloud.starters.face.verify.model.faceplus.FacePlusResponseInfo;
+import cn.com.qmth.examcloud.starters.face.verify.service.FaceVerifyService;
+import cn.com.qmth.examcloud.starters.face.verify.utils.HttpClientBuilder;
+import cn.com.qmth.examcloud.starters.face.verify.utils.JsonHelper;
+import cn.com.qmth.examcloud.starters.face.verify.utils.StrUtils;
+import okhttp3.*;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.Base64Utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static cn.com.qmth.examcloud.starters.face.verify.model.Constants.*;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class FaceVerifyServiceImpl implements FaceVerifyService {
+
+    private final static Logger log = LoggerFactory.getLogger(FaceVerifyServiceImpl.class);
+
+    private FaceVerifyProperties properties;
+
+    @Override
+    public IResult detectFaceByBaidu(String targetImageUrl) {
+        String accessToken = this.getAccessTokenByBaidu();
+        return this.detectFaceByBaidu(accessToken, targetImageUrl, false);
+    }
+
+    @Override
+    public IResult detectFaceByBaidu(byte[] imageFileBytes) {
+        String accessToken = this.getAccessTokenByBaidu();
+        String base64 = Base64Utils.encodeToString(imageFileBytes);
+        return this.detectFaceByBaidu(accessToken, base64, true);
+    }
+
+    @Override
+    public IResult detectFaceByBaidu(File targetImageFile) {
+        String accessToken = this.getAccessTokenByBaidu();
+        String base64 = StrUtils.toBase64(targetImageFile);
+        return this.detectFaceByBaidu(accessToken, base64, true);
+    }
+
+    private IResult detectFaceByBaidu(String accessToken, String targetImage, boolean isBases64) {
+        if (StringUtils.isBlank(accessToken)) {
+            throw new IllegalArgumentException("Baidu accessToken must be not empty.");
+        }
+
+        if (StringUtils.isBlank(targetImage)) {
+            throw new IllegalArgumentException("targetImage must be not empty.");
+        }
+
+        List params = new ArrayList();
+        Map<String, String> values = new TreeMap<>();
+        if (isBases64) {
+            values.put("image_type", "BASE64");
+        } else {
+            values.put("image_type", "URL");
+        }
+        values.put("image", targetImage);
+        params.add(values);
+
+        JsonHelper jsonMapper = new JsonHelper();
+        String jsonParams = jsonMapper.toJson(params);
+
+        String requestUrl = String.format(BAI_DU_DETECT_FACE_URL, accessToken);
+        IResult result = this.callForBaidu(jsonMapper, jsonParams, requestUrl);
+
+        if (result != null && result.isTokenExpired()) {
+            //Token过期,则重刷
+            accessToken = refreshAccessTokenByBaidu();
+            requestUrl = String.format(BAI_DU_DETECT_FACE_URL, accessToken);
+            return this.callForBaidu(jsonMapper, jsonParams, requestUrl);
+        }
+
+        return result;
+    }
+
+    @Override
+    public IResult detectFaceWithFaceSizeByBaidu(File targetImageFile) {
+        if (targetImageFile == null) {
+            throw new IllegalArgumentException("targetImageFile must be not empty.");
+        }
+
+        byte[] targetBytes;
+        try (InputStream targetStream = new FileInputStream(targetImageFile);) {
+            targetBytes = IOUtils.toByteArray(targetStream);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+            return null;
+        }
+
+        return this.detectFaceWithFaceSizeByBaidu(targetBytes);
+    }
+
+    @Override
+    public IResult detectFaceWithFaceSizeByBaidu(byte[] imageFileBytes) {
+        String accessToken = this.getAccessTokenByBaidu();
+        String base64 = Base64Utils.encodeToString(imageFileBytes);
+        Map<String, String> params = new TreeMap<>();
+        params.put("max_face_num", "2");// 最多处理人脸的数目(默认值为1)
+        params.put("image_type", "BASE64");
+        params.put("image", base64);
+
+        JsonHelper jsonMapper = new JsonHelper();
+        String jsonParams = jsonMapper.toJson(params);
+
+        String requestUrl = String.format(BAIDU_DETECT_FACE_WITH_FACE_SIZE_URL, accessToken);
+        IResult result = this.callForBaidu(jsonMapper, jsonParams, requestUrl);
+
+        if (result != null && result.isTokenExpired()) {
+            //Token过期,则重刷
+            accessToken = this.refreshAccessTokenByBaidu();
+            requestUrl = String.format(BAIDU_DETECT_FACE_WITH_FACE_SIZE_URL, accessToken);
+            return this.callForBaidu(jsonMapper, jsonParams, requestUrl);
+        }
+
+        return result;
+    }
+
+    private String getAccessTokenByBaidu() {
+        if (BaiduAccessToken.isNotEmpty()) {
+            return BaiduAccessToken.getValue();
+        }
+
+        BaiduAccessInfo info = this.loadAccessTokenByBaidu();
+        if (info == null || info.getAccess_token() == null) {
+            throw new IllegalArgumentException("Baidu loadAccessToken fail.");
+        }
+
+        BaiduAccessToken.setValue(info.getAccess_token());
+        return info.getAccess_token();
+    }
+
+    private String refreshAccessTokenByBaidu() {
+        BaiduAccessToken.setValue(null);
+        return this.getAccessTokenByBaidu();
+    }
+
+    private BaiduAccessInfo loadAccessTokenByBaidu() {
+        if (StringUtils.isBlank(properties.getBaiduKey()) || StringUtils.isBlank(properties.getBaiduSecret())) {
+            throw new IllegalArgumentException("Baidu apiKey or apiSecret must be not empty.");
+        }
+
+        String requestUrl = String.format(BAI_DU_TOKEN_URL, properties.getBaiduKey(), properties.getBaiduSecret());
+        Request.Builder request = new Request.Builder().url(requestUrl).get();
+
+        try (Response response = HttpClientBuilder.getClient().newCall(request.build()).execute();
+             ResponseBody body = response.body();) {
+            String bodyStr = body.string();
+
+            if (log.isDebugEnabled()) {
+                log.debug("[response] code: " + response.code() + " body: " + bodyStr);
+            }
+
+            if (response.isSuccessful()) {
+                return new JsonHelper().parseJson(bodyStr, BaiduAccessInfo.class);
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+        return null;
+    }
+
+    private BaiduResponseInfo callForBaidu(JsonHelper jsonMapper, String jsonParams, String requestUrl) {
+        final String contentType = "application/json; charset=utf-8";
+        RequestBody formBody = FormBody.create(MediaType.parse(contentType), jsonParams);
+        Request.Builder request = new Request.Builder().url(requestUrl).post(formBody);
+
+        try (Response response = HttpClientBuilder.getClient().newCall(request.build()).execute();
+             ResponseBody body = response.body();) {
+            String bodyStr = body.string();
+
+            if (log.isDebugEnabled()) {
+                log.debug("[response] code: " + response.code() + " body: " + bodyStr);
+            }
+
+            if (response.isSuccessful()) {
+                return jsonMapper.parseJson(bodyStr, BaiduResponseInfo.class);
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+        return null;
+    }
+
+
+    @Override
+    public IResult detectFaceByFacePlus(File targetImageFile) {
+        if (targetImageFile == null) {
+            throw new IllegalArgumentException("targetImageFile must be not empty.");
+        }
+
+        byte[] targetBytes;
+        try (InputStream targetStream = new FileInputStream(targetImageFile);) {
+            targetBytes = IOUtils.toByteArray(targetStream);
+        } catch (IOException e) {
+            log.error(e.getMessage());
+            return null;
+        }
+
+        return this.detectFaceByFacePlus(targetBytes, targetImageFile.getName());
+    }
+
+    @Override
+    public IResult detectFaceByFacePlus(byte[] imageFileBytes, String imageFileName) {
+        if (StringUtils.isBlank(properties.getFacePlusKey()) || StringUtils.isBlank(properties.getFacePlusSecret())) {
+            throw new IllegalArgumentException("Face++ apiKey or apiSecret must be not empty.");
+        }
+
+        if (imageFileBytes == null || imageFileBytes.length == 0) {
+            throw new IllegalArgumentException("fileBytes must be not empty.");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("[request] " + FACE_PLUS_DETECT_FACE_URL + " imageFileName: " + imageFileName);
+        }
+
+        RequestBody targetBody = RequestBody.create(MediaType.parse("application/octet-stream"), imageFileBytes);
+        MultipartBody.Builder formBody = new MultipartBody.Builder().setType(MultipartBody.FORM)
+                .addFormDataPart("api_key", properties.getFacePlusKey())
+                .addFormDataPart("api_secret", properties.getFacePlusSecret())
+                .addFormDataPart("image_file", StrUtils.urlEncode(imageFileName), targetBody);
+        return callForFacePlus(FACE_PLUS_DETECT_FACE_URL, formBody.build());
+    }
+
+    @Override
+    public IResult compareFaceByFacePlus(String sourceImageUrl, String targetImageUrl) {
+        if (StringUtils.isBlank(properties.getFacePlusKey()) || StringUtils.isBlank(properties.getFacePlusSecret())) {
+            throw new IllegalArgumentException("Face++ apiKey or apiSecret must be not empty.");
+        }
+
+        if (StringUtils.isBlank(sourceImageUrl) || StringUtils.isBlank(targetImageUrl)) {
+            throw new IllegalArgumentException("sourceImageUrl or targetImageUrl must be not empty.");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("[request] " + FACE_PLUS_COMPARE_FACE_URL + " sourceImageUrl: " + sourceImageUrl + " targetImageUrl: " + targetImageUrl);
+        }
+
+        RequestBody formBody = new FormBody.Builder()
+                .add("api_key", properties.getFacePlusKey())
+                .add("api_secret", properties.getFacePlusSecret())
+                .add("image_url1", sourceImageUrl)
+                .add("image_url2", targetImageUrl)
+                .build();
+        return callForFacePlus(FACE_PLUS_COMPARE_FACE_URL, formBody);
+    }
+
+    @Override
+    public IResult compareFaceByFacePlus(File sourceImageFile, File targetImageFile) {
+        if (StringUtils.isBlank(properties.getFacePlusKey()) || StringUtils.isBlank(properties.getFacePlusSecret())) {
+            throw new IllegalArgumentException("Face++ apiKey or apiSecret must be not empty.");
+        }
+
+        if (sourceImageFile == null || targetImageFile == null) {
+            throw new IllegalArgumentException("sourceImageFile or targetImageFile must be not empty.");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("[request] " + FACE_PLUS_COMPARE_FACE_URL + " sourceImageFile: " + sourceImageFile.getName() + " targetImageFile: " + targetImageFile.getName());
+        }
+
+        byte[] sourceBytes, targetBytes;
+        try (InputStream sourceStream = new FileInputStream(sourceImageFile);
+             InputStream targetStream = new FileInputStream(targetImageFile);) {
+            sourceBytes = IOUtils.toByteArray(sourceStream);
+            targetBytes = IOUtils.toByteArray(targetStream);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+            return null;
+        }
+
+        RequestBody sourceBody = RequestBody.create(MediaType.parse("application/octet-stream"), sourceBytes);
+        RequestBody targetBody = RequestBody.create(MediaType.parse("application/octet-stream"), targetBytes);
+        MultipartBody.Builder formBody = new MultipartBody.Builder().setType(MultipartBody.FORM)
+                .addFormDataPart("api_key", properties.getFacePlusKey())
+                .addFormDataPart("api_secret", properties.getFacePlusSecret())
+                .addFormDataPart("image_file1", StrUtils.urlEncode(sourceImageFile.getName()), sourceBody)
+                .addFormDataPart("image_file2", StrUtils.urlEncode(targetImageFile.getName()), targetBody);
+        return callForFacePlus(FACE_PLUS_COMPARE_FACE_URL, formBody.build());
+    }
+
+    @Override
+    public IResult compareFaceByFacePlus(String sourceImageUrl, File targetImageFile) {
+        if (StringUtils.isBlank(properties.getFacePlusKey()) || StringUtils.isBlank(properties.getFacePlusSecret())) {
+            throw new IllegalArgumentException("Face++ apiKey or apiSecret must be not empty.");
+        }
+
+        if (StringUtils.isBlank(sourceImageUrl) || targetImageFile == null) {
+            throw new IllegalArgumentException("sourceImageUrl or targetImageFile must be not empty.");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("[request] " + FACE_PLUS_COMPARE_FACE_URL + " sourceImageUrl: " + sourceImageUrl + " targetImageFile: " + targetImageFile.getName());
+        }
+
+        byte[] targetBytes;
+        try (InputStream targetStream = new FileInputStream(targetImageFile);) {
+            targetBytes = IOUtils.toByteArray(targetStream);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+            return null;
+        }
+
+        RequestBody targetBody = RequestBody.create(MediaType.parse("application/octet-stream"), targetBytes);
+        MultipartBody.Builder formBody = new MultipartBody.Builder().setType(MultipartBody.FORM)
+                .addFormDataPart("api_key", properties.getFacePlusKey())
+                .addFormDataPart("api_secret", properties.getFacePlusSecret())
+                .addFormDataPart("image_url1", sourceImageUrl)
+                .addFormDataPart("image_file2", StrUtils.urlEncode(targetImageFile.getName()), targetBody);
+        return callForFacePlus(FACE_PLUS_COMPARE_FACE_URL, formBody.build());
+    }
+
+    private FacePlusResponseInfo callForFacePlus(String url, RequestBody formBody) {
+        Request.Builder request = new Request.Builder().url(url).post(formBody);
+
+        try (Response response = HttpClientBuilder.getClient().newCall(request.build()).execute();
+             ResponseBody body = response.body();) {
+            String bodyStr = body.string();
+
+            if (log.isDebugEnabled()) {
+                log.debug("[response] code: " + response.code() + " body: " + bodyStr);
+            }
+
+            //403有并发超限情况
+            if (response.code() == 403 || response.isSuccessful()) {
+                return new JsonHelper().parseJson(bodyStr, FacePlusResponseInfo.class);
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+        return null;
+    }
+
+    public void setProperties(FaceVerifyProperties properties) {
+        this.properties = properties;
+    }
+
+}

+ 91 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/utils/HttpClientBuilder.java

@@ -0,0 +1,91 @@
+package cn.com.qmth.examcloud.starters.face.verify.utils;
+
+import okhttp3.ConnectionPool;
+import okhttp3.OkHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.net.ssl.*;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+
+public class HttpClientBuilder {
+
+    private static final Logger log = LoggerFactory.getLogger(HttpClientBuilder.class);
+
+    private static OkHttpClient client;
+
+    static {
+        client = Client.INSTANCE.getInstance();
+    }
+
+    public static OkHttpClient getClient() {
+        return client;
+    }
+
+    private enum Client {
+
+        INSTANCE;
+
+        Client() {
+            ConnectionPool connectionPool = new ConnectionPool(10, 5L, TimeUnit.MINUTES);
+
+            instance = new OkHttpClient
+                    .Builder()
+                    // .retryOnConnectionFailure(false)
+                    .connectionPool(connectionPool)
+                    .connectTimeout(30, TimeUnit.SECONDS)
+                    .readTimeout(60, TimeUnit.SECONDS)
+                    .writeTimeout(60, TimeUnit.SECONDS)
+                    .sslSocketFactory(sslSocketFactory(), trustAllCert())
+                    .hostnameVerifier(trustAllHost())
+                    .build();
+
+            log.debug("OkHttpClient init..");
+        }
+
+        private OkHttpClient instance;
+
+        public OkHttpClient getInstance() {
+            return instance;
+        }
+    }
+
+    private static SSLSocketFactory sslSocketFactory() {
+        try {
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, new TrustManager[]{trustAllCert()}, new SecureRandom());
+            return sslContext.getSocketFactory();
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static HostnameVerifier trustAllHost() {
+        return (hostname, sslSession) -> true;
+    }
+
+    private static X509TrustManager trustAllCert() {
+        return new X509TrustManager() {
+
+            @Override
+            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+
+            }
+
+            @Override
+            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+
+            }
+
+            @Override
+            public X509Certificate[] getAcceptedIssuers() {
+                return new X509Certificate[0];
+            }
+        };
+    }
+
+}

+ 69 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/utils/JsonHelper.java

@@ -0,0 +1,69 @@
+package cn.com.qmth.examcloud.starters.face.verify.utils;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+
+@SuppressWarnings("unchecked")
+public class JsonHelper {
+
+    private static Logger log = LoggerFactory.getLogger(JsonHelper.class);
+
+    private ObjectMapper mapper;
+
+    public JsonHelper() {
+        this(null);
+    }
+
+    public JsonHelper(Include include) {
+        mapper = new ObjectMapper();
+
+        //设置输出时包含属性的风格
+        if (include != null) {
+            mapper.setSerializationInclusion(include);
+        }
+
+        //设置输入时忽略在JSON字符串中存在但Java对象实际没有的属性
+        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+
+        //忽略无法转换的对象
+        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+
+        //设置默认日期格式
+        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
+    }
+
+    public String toJson(Object object) {
+        if (object == null) {
+            return null;
+        }
+
+        try {
+            return mapper.writeValueAsString(object);
+        } catch (IOException e) {
+            log.error("toJson error!" + e.getMessage(), e);
+            return null;
+        }
+    }
+
+    public <T> T parseJson(String jsonStr, Class<T> clazz) {
+        if (StringUtils.isEmpty(jsonStr)) {
+            return null;
+        }
+
+        try {
+            return mapper.readValue(jsonStr, clazz);
+        } catch (IOException e) {
+            log.error("parseJson error!" + e.getMessage(), e);
+            return null;
+        }
+    }
+
+}

+ 50 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/java/cn/com/qmth/examcloud/starters/face/verify/utils/StrUtils.java

@@ -0,0 +1,50 @@
+package cn.com.qmth.examcloud.starters.face.verify.utils;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.file.Files;
+
+/**
+ * @author: QMTH
+ * @since: 2019/3/25
+ */
+public class StrUtils {
+
+    private final static Logger log = LoggerFactory.getLogger(StrUtils.class);
+
+    public static String toBase64(File imageFile) {
+        if (imageFile == null) {
+            return "";
+        }
+
+        try (InputStream is = Files.newInputStream(imageFile.toPath());) {
+            byte[] bytes = IOUtils.toByteArray(is);
+            return Base64.encodeBase64String(bytes);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public static String urlEncode(String value) {
+        if (value == null) {
+            return "";
+        }
+
+        try {
+            return URLEncoder.encode(value, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            log.error(e.getMessage(), e);
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+}

+ 2 - 0
examcloud-starters/examcloud-face-verify-starter/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+cn.com.qmth.examcloud.starters.face.verify.FaceVerifyAutoConfiguration

+ 1 - 0
examcloud-starters/pom.xml

@@ -14,6 +14,7 @@
     <modules>
         <module>examcloud-geetest-starter</module>
         <module>examcloud-crypto-starter</module>
+        <module>examcloud-face-verify-starter</module>
     </modules>
 
 </project>