# 接入指南

# 平台概述

作为国内领先的网络身份服务商,识蛛可信身份服务平台整合人脸、声纹等生物识别技术、软证书、硬证书、手机验证码等多种认证方式,为不同行业客户提供高效的互联网身份认证服务,实现从身份认证到签名签章,从数据保全到电子公证,覆盖桌面端和移动端的完整解决方案。

# 产品介绍

  • 实名校验: 主要用于对用户证件信息进行验证。核实用户的姓名和身份证号码是否真实匹配、存在,从而有效防止身份造假,确保用户身份真实存在。
  • 人脸验证: 验证当前采集的人像是否与身份证芯片照为同一人。
  • 手机号实名认证: 主要验证用户,身份证,电话号码是否为同一人。
  • 活体信息验证: 是指用户在打开摄像头时,需要根据提示进行互动操作(例如,凝视屏幕、张嘴、摇头、眨眼睛等动作),从而达到鉴别真人的目的,可有效抵御照片、换脸、面具、遮挡以及屏幕翻拍等常见的攻击手段,从而帮助用户甄别欺诈行为,保障用户的利益。
  • 图片OCR识别:提供对身份证照片,营业执照等证件的识别服务。

# 基础概念解释

1、公共参数: 公共请求参数是指每个接口都需要使用到的请求参数,与业务无关;

2、业务参数: 根据调用API服务接口的需求所传递的参数;

3、签名算法: 签名算法是指数字签名的算法。数字签名(又称公钥数字签名、电子签章)是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术实现,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。

4、表单: 在网页中主要负责数据采集功能。一个表单有三个基本组成部分: 表单标签:这里面包含了处理表单数据所用CGI程序的URL以及数据提交到服务器的方法。 表单域:包含了文本框、密码框、隐藏域、多行文本框、复选框、单选框、下拉选择框和文件上传框等。 表单按钮:包括提交按钮、复位按钮和一般按钮;用于将数据传送到服务器上的CGI脚本或者取消输入,还可以用表单按钮来控制其他定义了处理脚本的处理工作。

# 可信时间戳

TIP

时间戳(timestamp),一个能表示一份数据在某个特定时间之前已经存在的、 完整的、 可验证的数据,通常是一个字符序列,唯一地标识某一刻的时间。使用数字签名技术产生的数据, 签名的对象包括了原始文件信息、 签名参数、 签名时间等信息。广泛的运用在知识产权保护、 合同签字、 金融帐务、 电子报价投标、 股票交易等方面。

壹证通提供时间戳服务接口,即为用户提供的摘要数据(SM3算法计算生成)加盖可信时间戳。可信时间戳必须核验用户身份,核验用户身份的方式分为三种:

  1. 基于身份证姓名+身份证号码的简单认证方式。
  2. 基于身份证姓名+身份证号码+人脸照片的组合认证方式。
  3. 基于身份证姓名+身份证号码+包含人脸活体视频的组合认证方式

注意:正确获取可信时间戳必须具备两个条件: 1)用户身份核验通过。 2)调用API时传入正确格式的摘要数据

# API签名算法

认证平台的 API 是基于 HTTP(S) 协议来调用的,开发者可以直接使用我们提供的SDK(包含了请求的封装,签名加密,响应解释等)来调用, 以下主要是针对自行封装 HTTP(S) 请求进行API调用的签名算法进行详细解说。API调用除了必须包含公共参数外,API本身业务级的参数,每个API的业务级参数请参考API文档说明。

# 签名算法原理

为了防止 API 调用过程中被恶意篡改,调用任何一个 API 都需要携带签名,服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝。目前支持的签名算法:HMAC-SHA256(signMethod=HMAC-SHA256),签名大体过程如下:

  1. 对所有 API 请求参数(包括公共参数和业务参数,但除去sign参数),根据参数名称的ASCII码表的顺序排序,将排序好的参数名和参数值拼接在一起。
  2. 拼接好的字符串和密钥分别按照UTF-8编码,用编码后的密钥字符流结合HmacSHA256算法对编码后的参数字符流进行摘要。
  3. 将摘要后的字符流转换为十六进制大写字符串,即得到签名值。

# JAVA 签名示例代码

姓名实名认证签名例子:

请求示例:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

/**
* 获取参数签名
* @author 壹证通
*/
public class GetSignature {

  /**密钥*/
  private static String secretKey = "XXX";
  private static String charset = "UTF-8";
  private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss";
  private static final String ALGORITHM = "HmacSHA256";
  private static final Object LOCK = new Object();
  private static Mac macInstance;

  public static void main(String[] args) {

    Map<String, String> paramMap = new HashMap<String, String>(10);

    //公共参数
    paramMap.put("appKey", "XXX");
    paramMap.put("signMethod", "HMAC-SHA256");
    paramMap.put("signVersion", "1");
    paramMap.put("method", "XXX");
    paramMap.put("format", "JSON");
    paramMap.put("timestamp", getTime());
    paramMap.put("version", "1");
    paramMap.put("nonce", getNonce());

    //具体接口参数,如姓名实名认证
    paramMap.put("realname", "XXX");
    paramMap.put("idcard", "XXX");

    //获取待签名内容,排序
    String signContent = getSignatureContent(paramMap);
    System.out.println("待签名内容:" + signContent);

    //计算签名
    String sign = computeSignature(secretKey, signContent, charset);
    System.out.println("签名后:" + sign);
  }

  /**
  * 格式化时间
  */
  private static String getTime() {
    TimeZone tz = TimeZone.getTimeZone("UTC");
    DateFormat df = new SimpleDateFormat(TIMESTAMP_FORMAT);
    df.setTimeZone(tz);
    return df.format(new Date());
  }

  /**
  * 生成随机数
  */
  private static String getNonce(){
    Random random = new Random();
    return String.valueOf(random.nextInt(1000000000));
  }

  /**
  * 将参数按key值排序
  */
  public static String getSignatureContent(Map<String, String> paramMap) {

    Collection<String> keySet = paramMap.keySet();
    //签名内容
    StringBuilder content = new StringBuilder();
    //所有的键值
    List<String> keys = new ArrayList<String>(keySet);
    //排序
    Collections.sort(keys);

    //循环赋值
    for (String key : keys) {
      String value = paramMap.get(key);
      if (isNotEmpty(key) && isNotEmpty(value)) {
        content.append(key).append(value);
      }
    }

    return content.toString();
  }

  /**
  *字符串非空校验
  */
  private static boolean isNotEmpty(String str) {
    return str != null && str.length() != 0;
  }

  /**
  * 计算签名值
  */

  private static String computeSignature(String key, String data, String charset) {
    try {
      byte[] signData = sign(key.getBytes(charset), data.getBytes(charset));
      return byte2hex(signData);
    } catch (UnsupportedEncodingException ex) {
      throw new RuntimeException("不支持的算法: " + charset, ex);
    }
  }

  /**
  * 把字节流转换为十六进制表示方式。
  */
  private static String byte2hex(byte[] bytes) {
    StringBuilder sign = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
      String hex = Integer.toHexString(bytes[i] & 0xFF);
      if (hex.length() == 1) {
        sign.append("0");
      }
      sign.append(hex.toUpperCase());
    }

    return sign.toString();
  }

  /**
  * 使用HMAC加密
  */
  private static byte[] sign(byte[] key, byte[] data) {
    try {
    //因为Mac类的getInstance()方法的调用时一个同步方法,可能被阻塞,所以使用原型模式来提高可靠性
      if (macInstance == null) {
       synchronized (LOCK) {
        if (macInstance == null) {
          macInstance = Mac.getInstance(ALGORITHM);
        }
      }
    }

    Mac mac;
    try {
      mac = (Mac) macInstance.clone();
    } catch (CloneNotSupportedException e) {
      //如果不可复制,创建一个新的Mac对象
      mac = Mac.getInstance(ALGORITHM);
    }

    mac.init(new SecretKeySpec(key, ALGORITHM));
    return mac.doFinal(data);
    } catch (NoSuchAlgorithmException ex) {
      throw new RuntimeException("不支持的算法: " + ALGORITHM, ex);
    } catch (InvalidKeyException ex) {
      throw new RuntimeException("非法key: " + key, ex);
    }
  }
}

详细示例代码请参见 SDK 源代码。

# 调用示例

1、设置参数值

 format = "JSON"
 version = "1"
 appKey = "1111111"
 signMethod = "HMAC-SHA256"
 signVersion = "1"
 signVersion = "1"
 timestamp = "2018-02-07 02:50:21"
 nonce = "随机数"
 realname = "张三"
 idcard = "111111111111111111"

2、排序

 appKey = "1111111"
 format = "JSON"
 idcard = "111111111111111111"
 method = "realid.idcard.verify"
 nonce ="随机数"
 realname = "张三"
 signMethod = "HMAC-SHA256"
 signVersion = "1"
 timestamp = "2018-02-07 02:50:21"
 version = "1"

3、拼接参数名与参数值

appKey1111111formatJSONidcard111111111111111111methodrealid.idcard.verifynonce1111111 realname张三signMethodHMAC-SHA256signVersion1timestamp2018-02-07 02:50:21version1

4、生成签名

假设 secretKey 为 111111,则签名结果为: E41E6FDA4D24B27AE78281F6D71D790F55097CD558BB377A3F9343F07ADED112

# API调用协议

接口支持HTTP,HTTPS GET/POST请求,所有接口需在请求中加入公共参数,请求及返回结果都使用 UTF-8 字符集进行编码。

组装 HTTP(S) 请求

将所有参数名和参数值采用UTF-8进行 URL 编码(参数顺序可随意,但必须要包括签名参数),然后通过GETPOST发起请求,如:

HTTP GET请求

http://api.spiderid.cn/api/router/rest?appKey=1111111&format=JSON&method=realid.idcard.verify&signMethod=HMAC-SHA256&signVersion=1&version=1&realname=张三&idcard=111111111111111111&nonce=随机数&timestamp=2018-02-07&nbsp;02:50:21&sign=E41E6FDA4D24B27AE78281F6D71D790F55097CD558BB377A3F9343F07ADED112

HTTPS POST请求

https://api.spiderid.cn/api/router/rest?appKey=1111111&format=JSON&method=realid.idcard.verify&signMethod=HMAC-SHA256&signVersion=1&version=1&nonce=1111111&timestamp=2018-02-07&nbsp;02:50:21&sign=E41E6FDA4D24B27AE78281F6D71D790F55097CD558BB377A3F9343F07ADED112

realname=张三&idcard=111111111111111111

# 注意事项

1、所有的请求和响应数据编码皆为UTF-8格式,URL 里的所有参数名和参数值请做 URL 编码。如果请求的 Content-Type 是 application/x-www-form-urlencoded,则 HTTP Body 体里的所有参数值也做 URL 编码;如果是 multipart/form-data 格式,每个表单字段的参数值无需编码, 但每个表单字段的 charset 部分需要指定为UTF-8。

2、参数名与参数值拼装起来的 URL 长度小于 1024 个字符时,可以用 GET 发起请求;参数类型含 byte[] 类型或拼装好的请求 URL 过长时,必须用 POST 发起请求。所有 API 都可以用 POST 发起请求,某些 API 只支持 POST 请求

3、POST请求请务必将业务参数放入请求Body中

# 公共参数

公共请求参数是指每个接口都需要使用到的请求参数,务必以url参数形式传入

名称 类型 是否必须 描述
appKey String 身份标识,注册后获得
sign String 签名结果串,请参看API签名机制
signMethod String 签名算法,默认HMAC-SHA256
signVersion String 签名算法版本,目前是1
method String 服务方法/API接口名称
format String 返回值的类型,默认JSON
timestamp String 时间戳,日期格式按照 ISO8601 标准表示,并需要使用 UTC 时间。格式为 yyyy-MM-dd HH:mm:ss
nonce String 唯一随机数,同样的值,10分钟内只能被使用一次
version String API 版本号,目前版本是1

请求示例:

GET:

http://api.spiderid.cn/api/router/rest?
&appKey=XXX
&sign=XXX
&signMethod=HMAC-SHA256
&signVersion=1
&method=XXX
&format=JSON
&timestamp=2018-02-07 02:50:21
&nonce=随机数
&version=1
&<[具体接口特有的请求参数]>

POST:

http://api.spiderid.cn/api/router/rest?
&appKey=XXX
&sign=XXX
&signMethod=HMAC-SHA256
&signVersion=1
&method=XXX
&format=JSON
&timestamp=2018-02-07 02:50:21
&nonce=随机数
&version=1

<[具体接口特有的请求参数]>

# 响应参数

调用 API 服务后返回数据采用统一格式,code0 ,请求成功,其他为失败,这时没有data结果信息

名称 类型 描述
code Integer 状态码,0请求成功,其他为失败,具体见 API错误码说明
requestId String 请求唯一标识
message String 状态码的描述
data Object 结果信息,code为0时出现,具体看各个接口说明

# 成功示例

JSON示例

{
  "code": 0,
  "requestId": "dsd24...",
  "data": {
      ......
  },
  "message": "success"
}

# 失败示例

JSON示例

{
  "code": 10008,
  "requestId": "f1001ac1224...",
  "message": "App不存在或状态异常"
}

# API错误码说明

# 错误码解释

返回码 系统错误 备注
10001 系统错误
10002 服务异常,请联系客服
10003 远程服务错误
10004 平台系统维护
10005 请求参数(XX)XX,请参考API文档 (如:请求参数(name)不合法,请参考API文档)
10006 请求参数非法,请参考API文档
10007 签名算法XX不支持 (如:签名算法HMAC-SHA1不支持)
10008 App不存在或状态异常
10009 App签名错误
10010 请求重复
10011 请求过期
10012 没有访问此接口权限
10013 请求IP(XX)不在允许访问接口白名单 (如:请求IP(127.0.0.1)不在允许访问接口白名单)
10014 请求超时
10015 API超过最大可访问次数
10016 App已被禁用
10017 服务限流
10018 余额不足
10019 账户已冻结
10020 请求数据过大
10021 该服务已经下架
10022 核验中心系统错误
10023 认证记录不存在
10024 认证状态非法
10025 认证场景非法
10026 核验中心系统维护
10027 tsp错误
10028 evc错误
10029 身份未认证
10030 用户缺失身份参数
10031 用户类型非法
10032 API不存在

# 图片处理

人脸比对等业务对上传的图片大小有限,为减少开发人员在此处花费的精力,我们提供了几种对图片操作的示例(包括对图片大小,格式等处理),以供开发者参考.

# 引入依赖

将下面这段代码复制到pom文件中即可

    <!--图片exif信息-->
    <dependency>
      <groupId>com.drewnoakes</groupId>
      <artifactId>metadata-extractor</artifactId>
      <version>2.11.0</version>
    </dependency>

    <!--图片工具类-->
    <dependency>
      <groupId>net.coobird</groupId>
      <artifactId>thumbnailator</artifactId>
      <version>0.4.8</version>
    </dependency>

# 图片工具类


import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifDirectoryBase;
import net.coobird.thumbnailator.Thumbnails;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * 图片处理
 */
public class ImageUtils {


    /**
     * 根据文件流读取图片文件真实类型
     *
     * @param bytes
     * @return
     */
    public static String getTypeByStream(byte[] bytes) {
        byte[] b = new byte[10];
        for (int i = 0; i < 10; i++) {
            b[i] = bytes[i];
        }
        String type = bytesToHexString(b).toUpperCase();
        if (type.contains("FFD8FF")) {
            return "jpg,image/jpeg";
        } else if (type.startsWith("89504E47")) {
            return "png,image/png";
        } else if (type.startsWith("47494638")) {
            return "gif,image/gif";
        } else if (type.startsWith("49492A00")) {
            return "tif,image/tiff";
        } else if (type.startsWith("424D")) {
            return "bmp,image/bmp";
        } else {
            throw new IllegalArgumentException("解析出图片格式不合法");
        }
    }

    /**
     * byte数组转换成16进制字符串
     *
     * @param src
     * @return
     */
    public static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder();
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }

    /**
     * 获取图片Exif方向 (原始图片,即没有经过压缩旋转裁剪等操作)
     *
     * @return
     */
    public static int getExifOrientation(byte[] bytes) {
        int angel = 0;
        try {
            Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(bytes));
            for (Directory directory : metadata.getDirectories()) {
                for (Tag tag : directory.getTags()) {
                    if (tag.getTagType() == ExifDirectoryBase.TAG_ORIENTATION) {
                        String description = tag.getDescription();
                        if (description.contains("90")) {
                            // 顺时针旋转90度
                            angel = 90;
                        } else if (description.contains("180")) {
                            // 顺时针旋转180度
                            angel = 180;
                        } else if (description.contains("270")) {
                            // 顺时针旋转270度
                            angel = 270;
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        return angel;
    }


    /**
     * 图片裁剪
     *
     * @param bytes
     * @param x      X轴
     * @param y      Y轴
     * @param w      裁剪的宽
     * @param h      裁剪的高
     * @param width  宽缩放不超过width
     * @param height 高缩放不超过height
     * @param format 图片格式
     * @return
     */
    public static byte[] imageCut(byte[] bytes, int x, int y, int w, int h, int width, int height, String format) {
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            Thumbnails.of(new ByteArrayInputStream(bytes))
                    //指定坐标 w*y区域
                    .sourceRegion(x, y, w, y)
                    //宽缩小为<=width,高缩小为<=height
                    .size(width, height)
                    //不按照比例,指定大小进行缩放
                    .keepAspectRatio(false)
                    //输出格式
                    .outputFormat(format)
                    .toOutputStream(out);
            return out.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * 图片等比例压缩不超过指定大小
     *
     * @param bytes
     * @param size   指定大小
     * @param format 图片格式
     * @return
     */
    public static byte[] imageCompress(byte[] bytes, long size, String format) {
        try {
            if (bytes.length > size) {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                Thumbnails.of(new ByteArrayInputStream(bytes))
                        //缩小原图0.8
                        .scale(0.8)
                        //输出格式
                        .outputFormat(format)
                        .toOutputStream(out);
                return imageCompress(out.toByteArray(), size, format);
            }
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取图片Exif方向旋转,按照大小等比例压缩
     *
     * @param bytes
     * @param format 图片格式
     * @throws IOException
     */
    public static byte[] imageRotateAndCompress(byte[] bytes, String format) {
        try {
            //获取拍照时旋转角度
            int angel = getExifOrientation(bytes);
            double scale = 1.0;
            if ((bytes.length > 1024 * 100) && (bytes.length <= 1024 * 200)) {
                scale = 0.8;
            } else if ((bytes.length > 1024 * 200) && (bytes.length <= 1024 * 400)) {
                scale = 0.7;
            } else if ((bytes.length > 1024 * 400) && (bytes.length <= 1024 * 600)) {
                scale = 0.6;
            } else if ((bytes.length > 1024 * 600) && (bytes.length <= 1024 * 800)) {
                scale = 0.5;
            } else if ((bytes.length > 1024 * 800) && (bytes.length <= 1024 * 1024)) {
                scale = 0.3;
            } else if ((bytes.length > 1024 * 1024) && (bytes.length <= 1024 * 1024 * 3)) {
                scale = 0.2;
            } else if (bytes.length > 1024 * 1024 * 3) {
                scale = 0.1;
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            Thumbnails.of(new ByteArrayInputStream(bytes))
                    //rotate(角度),正数:顺时针 负数:逆时针
                    .rotate(angel)
                    //scale(比例),1等比,大于1放大,小于1缩小
                    .scale(scale)
                    //输出格式
                    .outputFormat(format)
                    .toOutputStream(out);
            return out.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

# 代码示例

    /**
     * 获取图片Exif方向 (原始图片,即没有经过压缩旋转裁剪等操作)
     *
     * @throws IOException
     */
    @Test
    public void test_getExifOrientation() throws IOException {
        //读取图片
        byte[] bytes = FileUtils.readFileToByteArray(new File("C:\\workspace\\real-identity-openapi-java-sdk\\src\\test\\resources\\data\\ll.JPG"));
        //获取拍照时旋转方向
        int angel = ImageUtils.getExifOrientation(bytes);
        System.out.println(angel);
    }

     /**
     * 图片压缩不超过指定大小
     *
     * @throws IOException
     */
    @Test
    public void test_imageCompress() throws IOException {
        //读取图片
        byte[] bytes = FileUtils.readFileToByteArray(new File("C:\\workspace\\real-identity-openapi-java-sdk\\src\\test\\resources\\data\\ll.JPG"));
        //获取图片类型
        String formatName = ImageUtils.getTypeByStream(bytes).split(",")[0];
        //图片压缩100KB以内,转JPEG格式
        byte[] res = ImageUtils.imageCompress(bytes, 1024 * 100, formatName);
        //转换成base64字符串
        String base64String = Base64.encodeBase64String(res);
        System.out.println(base64String);
    }

    /**
     * 图片裁剪
     *
     * @throws IOException
     */
    @Test
    public void test_imageCut() throws IOException {
        //读取图片
        byte[] bytes = FileUtils.readFileToByteArray(new File("C:\\workspace\\real-identity-openapi-java-sdk\\src\\test\\resources\\data\\ll.JPG"));
        //按指定坐标裁剪
        byte[] res = ImageUtils.imageCut(bytes, 200, 300, 400, 400, 200, 200, "JPEG");
        //转换成base64字符串
        String base64String = Base64.encodeBase64String(res);
        System.out.println(base64String);
    }


    /**
     * 图片旋转压缩,按照大小等比例压缩
     *
     * @throws IOException
     */
    @Test
    public void test_imageRotateAndCompress() throws IOException {
        //读取图片
        byte[] bytes = FileUtils.readFileToByteArray(new File("C:\\workspace\\real-identity-openapi-java-sdk\\src\\test\\resources\\data\\ll.JPG"));
        //图片旋转压缩,转JPEG格式
        byte[] res = ImageUtils.imageRotateAndCompress(bytes, "JPEG");
        //转换成base64字符串
        String base64String = Base64.encodeBase64String(res);
        System.out.println(base64String);
    }

# SM3摘要

import cn.unitid.easypki.security.sm3.SM3Digest;

import java.nio.charset.Charset;

public class Sm3Utils {

    public static byte[] sm3Digest(byte[] data) {
        SM3Digest sm3 = new SM3Digest();
        sm3.update(data, 0, data.length);
        byte[] out = new byte[32];
        sm3.doFinal(out, 0);
        return out;
    }

    public static byte[] sm3Digest(String data) {
        byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
        return sm3Digest(bytes);
    }
}
最后更新于: 1/15/2024, 1:07:47 PM