# 接入指南
# 平台概述
作为国内领先的网络身份服务商,识蛛可信身份服务平台整合人脸、声纹等生物识别技术、软证书、硬证书、手机验证码等多种认证方式,为不同行业客户提供高效的互联网身份认证服务,实现从身份认证到签名签章,从数据保全到电子公证,覆盖桌面端和移动端的完整解决方案。
# 产品介绍
- 实名校验: 主要用于对用户证件信息进行验证。核实用户的姓名和身份证号码是否真实匹配、存在,从而有效防止身份造假,确保用户身份真实存在。
- 人脸验证: 验证当前采集的人像是否与身份证芯片照为同一人。
- 手机号实名认证: 主要验证用户,身份证,电话号码是否为同一人。
- 活体信息验证: 是指用户在打开摄像头时,需要根据提示进行互动操作(例如,凝视屏幕、张嘴、摇头、眨眼睛等动作),从而达到鉴别真人的目的,可有效抵御照片、换脸、面具、遮挡以及屏幕翻拍等常见的攻击手段,从而帮助用户甄别欺诈行为,保障用户的利益。
- 图片OCR识别:提供对身份证照片,营业执照等证件的识别服务。
# 基础概念解释
1、公共参数: 公共请求参数是指每个接口都需要使用到的请求参数,与业务无关;
2、业务参数: 根据调用API服务接口的需求所传递的参数;
3、签名算法: 签名算法是指数字签名的算法。数字签名(又称公钥数字签名、电子签章)是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术实现,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。
4、表单: 在网页中主要负责数据采集功能。一个表单有三个基本组成部分: 表单标签:这里面包含了处理表单数据所用CGI程序的URL以及数据提交到服务器的方法。 表单域:包含了文本框、密码框、隐藏域、多行文本框、复选框、单选框、下拉选择框和文件上传框等。 表单按钮:包括提交按钮、复位按钮和一般按钮;用于将数据传送到服务器上的CGI脚本或者取消输入,还可以用表单按钮来控制其他定义了处理脚本的处理工作。
# 可信时间戳
TIP
时间戳(timestamp),一个能表示一份数据在某个特定时间之前已经存在的、 完整的、 可验证的数据,通常是一个字符序列,唯一地标识某一刻的时间。使用数字签名技术产生的数据, 签名的对象包括了原始文件信息、 签名参数、 签名时间等信息。广泛的运用在知识产权保护、 合同签字、 金融帐务、 电子报价投标、 股票交易等方面。
壹证通提供时间戳服务接口,即为用户提供的摘要数据(SM3算法计算生成)加盖可信时间戳。可信时间戳必须核验用户身份,核验用户身份的方式分为三种:
- 基于身份证姓名+身份证号码的简单认证方式。
- 基于身份证姓名+身份证号码+人脸照片的组合认证方式。
- 基于身份证姓名+身份证号码+包含人脸活体视频的组合认证方式
注意:正确获取可信时间戳必须具备两个条件: 1)用户身份核验通过。 2)调用API时传入正确格式的摘要数据
# API签名算法
认证平台的 API 是基于 HTTP(S) 协议来调用的,开发者可以直接使用我们提供的SDK(包含了请求的封装,签名加密,响应解释等)来调用, 以下主要是针对自行封装 HTTP(S) 请求进行API调用的签名算法进行详细解说。API调用除了必须包含公共参数外,API本身业务级的参数
,每个API的业务级参数请参考API文档说明。
# 签名算法原理
为了防止 API 调用过程中被恶意篡改,调用任何一个 API 都需要携带签名,服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝。目前支持的签名算法:HMAC-SHA256(signMethod=HMAC-SHA256),签名大体过程如下:
- 对所有 API 请求参数(包括公共参数和
业务参数
,但除去sign参数),根据参数名称的ASCII码表的顺序排序,将排序好的参数名和参数值拼接在一起。 - 拼接好的字符串和密钥分别按照UTF-8编码,用编码后的密钥字符流结合HmacSHA256算法对编码后的参数字符流进行摘要。
- 将摘要后的字符流转换为
十六进制大写字符串
,即得到签名值。
# 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 编码(参数顺序可随意,但必须要包括签名参数),然后通过GET
或POST
发起请求,如:
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=随机数×tamp=2018-02-07 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×tamp=2018-02-07 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
×tamp=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
×tamp=2018-02-07 02:50:21
&nonce=随机数
&version=1
<[具体接口特有的请求参数]>
# 响应参数
调用 API 服务后返回数据采用统一格式,code
为0
,请求成功,其他为失败,这时没有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);
}
}
← 介绍 实名身份时间戳/数字证书服务API →