1. 接口鉴权
1.1. 鉴权方式
OpenAPI请求采用接口签名方式,需要在HTTP HEADER中加入以下请求头:
序号 | header名 | 描述 |
---|---|---|
1 | ctimestamp | 客户端本地unix毫秒时间戳 |
2 | cnonce | 随机字符串,可含字母,客户端自行生成 |
3 | cappkey | 标识第三方系统身份,由鸿图分配 |
4 | csign | 按签名规则计算的签名 |
5 | zoneUuid | 区域uuid(选填) |
1.2. 签名计算规则
签名算法
- 签名算法:MD5,32位小写
- sign计算规则:csign=MD5(uri-method-queryParamStr-requestBodyMD5-secretKey-ctimestamp-cnonce-cappkey)
- 数据解释:
- uri:必须,请求URI,以"/"开头,如:"/v1/api/person/list"
- method:必须,请求方式,全大写,如: "POST"
- queryParamStr:非必须,query域传值,有值传形如"name=张三&age=28&..."字符串;无值传""
- requestBodyMD5:非必须,仅文件上传接口可为空;否则为JSON数据或"{}"的MD5加密串(为空会报JSON解析异常)
- secretKey:必须,鸿图平台提供的签名私钥
- ctimestamp:必须,当前时间戳
- cnonce:必须,随机码
- cappkey:必须,鸿图平台提供的签名公钥
- csign:生成的最终MD5签名串
- 示例:
- uri="/v1/api/person/list"
- method="POST"
- queryParamStr=""
- requestBodyMD5="dcb8b5bfe22ef26821b9b34e650c534b"
- secretKey="sdfajk3242324fa!djq7"
- ctimestamp="1627300328673"
- cnonce="1234344"
- cappkey="appkey1"
- 数据解释:
1.3. 签名示例
以人员列表查询为例,通过在线Demo计算签名,Demo地址
请求参数如下:
- uri="/v1/api/person/list"
- method="POST"
- requestBody(请求体):
{"pageNum":1,"pageSize":10}
则requestBodyMD5为:675c299bcd88edf31c09c48f9c7fba4b
- secretKey="sdfajk3242324fa!djq7"
- ctimestamp="1627300328673"
- cnonce="1234344"
- cappkey="appkey1"
- 则csign=MD5("/v1/api/person/list-POST--675c299bcd88edf31c09c48f9c7fba4b-sdfajk3242324fa!djq7-1627300328673-1234344-appkey1"),结果为:52141def7a1e346ceee2f22c43d5fb97
那么,我们可以如下请求:
1.4. 可用签名及秘钥
目前,鸿图里面有两组cappkey和秘钥,如下:
cappkey | secret |
---|---|
appkey1 | sdfajk3242324fa!djq7 |
appkey2 | zxcv&jlkfd234adf#xi9 |
1.5. 签名SDK
1.5.1. Java版本
package com.megvii.sign.sdk;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
/**
描述:
Author:liuxing
Date:2020-09-11
*/
public class SignUtlis {
private static final String SIGN_FORMAT = "%s-%s-%s-%s-%s-%s-%s-%s";
/**
签名算法生成卡密昂
@param ctimestamp
@param cnonce
@param requestBody 请求体对象
@param requestParam
@param url
@param method
@param cappKey
@param secret
@return
*/
public static String sign(String ctimestamp, String cnonce, Object requestBody, String requestParam, String url, String method, String cappKey, String secret) {
String requestBodyMd5 = "";
if (requestBody != null) {
String requestBodyJson = JSONObject.toJSONString(requestBody);
if (StringUtils.isNotEmpty(requestBodyJson)) {
requestBodyMd5 = DigestUtil.encryptMd5(requestBodyJson);
}
}
String signStr = String.format(SIGN_FORMAT, url, method, requestParam, requestBodyMd5, secret, ctimestamp, cnonce, cappKey);
String sign = DigestUtil.encryptMd5(signStr);
return sign;
}
}
package com.megvii.sign.sdk;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class DigestUtil {
/** 算法名称 */
private static final String ALGORITHM = "DESede";
/** 16进制字母 */
private static final char[] HEX_DIGITS={'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
/**
* @param decript 要加密的字符串
* @return 加密的字符串
* SHA1加密
*/
public final static String SHA1(String decript) {
try {
MessageDigest digest = java.security.MessageDigest
.getInstance("SHA-1");
digest.update(decript.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
StringBuffer hexString = new StringBuffer();
// 字节数组转换为 十六进制 数
for (int i = 0; i < messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
return hexString.toString();
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
/**
* 3DES加密,key必须是长度大于等于 3*8 = 24 位
* @param src
* @param key
* @return
* @throws Exception
*/
public static String encrypt3DES(String src, String key) {
byte[] keyBytes = key.getBytes();
try {
DESedeKeySpec dks = new DESedeKeySpec(keyBytes);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
SecretKey securekey = keyFactory.generateSecret(dks);
Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, securekey);
byte[] b=cipher.doFinal(src.getBytes());
return StringUtil.byte2hex(b);
}
catch (Exception e) {
return null;
}
}
/**
* 3DESECB解密,key必须是长度大于等于 3*8 = 24 位
* @param src
* @param key
* @return
* @throws Exception
*/
public static String decrypt3DES(String src, String key) {
byte[] keyBytes = key.getBytes();
try {
//--通过base64,将字符串转成byte数组
byte[] bytesrc = StringUtil.hex2byte(src);
//--解密的key
DESedeKeySpec dks = new DESedeKeySpec(keyBytes);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
SecretKey securekey = keyFactory.generateSecret(dks);
//--Chipher对象解密
Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, securekey);
byte[] retbyte = cipher.doFinal(bytesrc);
return new String(retbyte);
}
catch (Exception e) {
return null;
}
}
/**
*
* Description:md5加密
* @Create_by:JH
* @Create_date:2014-9-9
* @Last_Edit_By:
* @Edit_Description
* @Create_Version:exinhua 1.0
*/
public static String encryptMd5(String src) {
if(StringUtil.isEmpty(src)) {
return src;
}
try {
byte[] btInput = src.getBytes();
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
return byteToHexString(md);
}
catch (Exception e) {
throw new RuntimeException("error occurated when encrypt", e);
}
}
/**
* 对输入进行sha1加密后,在进行16进制转换
* @param value
* @return
*/
public static String hexSHA1(String value) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(value.getBytes("utf-8"));
byte[] digest = md.digest();
return byteToHexString(digest);
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* 字节数组转换成16进制表示
* @param bytes
* @return
*/
public static String byteToHexString(byte[] bytes) {
int j = bytes.length;
char[] str = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = bytes[i];
str[k++] = HEX_DIGITS[byte0 >>> 4 & 0xf];
str[k++] = HEX_DIGITS[byte0 & 0xf];
}
return new String(str);
}
}
package com.megvii.sign.sdk;
/**
* 字符串工具类
*
*/
public class StringUtil {
public static final String[] HEX_ARRAYS = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
/**
* 判断value是否是null或者其length为0
*
* @param value
* @return
*/
public static Boolean isEmpty(String value) {
return value == null || value.length() == 0;
}
/**
* 判断value是否不是null且其length>0
*
* @param value
* @return
*/
public static Boolean isNotEmpty(String value) {
return !isEmpty(value);
}
/**
* trim操作
*
* @param value
* @return
*/
public static String trim(String value) {
if (value == null) {
return null;
}
return value.trim();
}
/**
* byte string to hex string
*
* @param b
* @return
*/
public static String byte2hex(byte[] b) {
if (b == null) {
return null;
}
if (b.length == 0) {
return "";
}
StringBuilder retBuilder = new StringBuilder();
for (int n = 0; n < b.length; ++n) {
retBuilder.append(HEX_ARRAYS[(b[n] & 0xF0) >> 4]);
retBuilder.append(HEX_ARRAYS[b[n] & 0x0F]);
}
return retBuilder.toString();
}
/**
* hex string to byte string
*
* @param str
* @return
*/
public static byte[] hex2byte(String str) {
if (str == null)
return null;
str = str.trim();
int len = str.length();
if ((len == 0) || (len % 2 == 1))
return null;
byte[] b = new byte[len / 2];
byte tmp = 0;
char[] strs = str.toCharArray();
for (int i = 0; i < len; i++) {
byte t = 0;
if (strs[i] >= '0' && strs[i] <= '9') {
t = (byte) (strs[i] - '0');
} else if (strs[i] >= 'A' && strs[i] <= 'F') {
t = (byte) (strs[i] - 'A' + 10);
} else if (strs[i] >= 'a' && strs[i] <= 'f') {
t = (byte) (strs[i] - 'a' + 10);
}
if ((i & 0x1) == 1) {
tmp <<= 4;
tmp += t;
b[i / 2] = tmp;
tmp = 0;
} else {
tmp = t;
}
}
return b;
}
/**
* 判断一个字符串是否只包含数字(10进制)字符
*
* @param str
* @return
*/
public static Boolean isNumeric(String str) {
if (isEmpty(str)) {
return false;
}
char[] arr = str.toCharArray();
for (char c : arr) {
if (!Character.isDigit(c)) {
return false;
}
}
return true;
}
/**
* 混淆字串串(显示前pn个字符和后tn个字符,其余全部用*填充)
*
* @param source
* @param pn
* @param tn
* @return
*/
public static String mix(String source, int pn, int tn) {
return mix(source, pn, tn, '*');
}
/**
* 混淆字串串(显示前pn个字符和后tn个字符,其余前部用mixChar填充)
*
* @param source
* @param pn
* @param tn
* @param mixChar
* @return
*/
public static String mix(String source, int pn, int tn, char mixchar) {
if (source == null || source.length() <= pn + tn) {
return source;
}
int len = source.length();
StringBuilder tmp = new StringBuilder(source.length());
char[] mobileAs = source.toCharArray();
for (int i = 0; i < pn; i++) {
tmp.append(mobileAs[i]);
}
for (int i = 0; i < len - (pn + tn); i++) {
tmp.append(mixchar);
}
for (int i = len - tn; i < len; i++) {
tmp.append(mobileAs[i]);
}
return tmp.toString();
}
/**
* 获取字符串长度(全角2,半角1)
* @param value
* @return
*/
public static final int getLength(String value) {
if(StringUtil.isEmpty(value)) {
return 0;
}
int len = 0;
for (char c : value.toCharArray()) {
len++;
if(isSbcCase(c)) {
len++;
}
}
return len;
}
/**
* 判断字符是否是半角
* @param c
* @return
*/
public static final Boolean isDbcCase(char c) {
int k = 0x80;
return c / k == 0 ? true : false;
}
/**
* 判断是否时全角
* @param c
* @return
*/
public static final Boolean isSbcCase(char c) {
return isDbcCase(c);
}
/**
* 判断是否都是数字
* @param value
* @return
*/
public static final Boolean isAllDigits(String value) {
if(StringUtil.isEmpty(value)) {
return true;
}
for (char c : value.toCharArray()) {
if(c < '0' || c > '9') {
return false;
}
}
return true;
}
/**
* 在指定的字符串前面填充0,直到字段串的长度达到指定的长度
* 示例:输入6, 2 ---> 返回06
* @param data 需要被填充的字符串
* @param fillStr 需要填充得字符串
* @param length 目标字符串的长度
* @return 返回填充后的字符串
*/
public static String frontFillStr(String data, String fillStr, int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length-data.length(); i++) {
sb.append(fillStr);
}
return sb.append(data).toString();
}
}
请求示例
package test;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 描述:
* Author:liuxing
* Date:2020-09-14
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DeviceReq {
private String name;
private Integer pageNum;
private Integer deviceType;
private Integer pageSize;
}
package test;
import com.alibaba.fastjson.JSONObject;
import com.megvii.sign.sdk.SignUtlis;
import okhttp3.*;
import java.util.concurrent.TimeUnit;
/**
* 描述: 模拟一次请求的过程
* Author:liuxing
* Date:2020-09-14
*/
public class SignUtilTest {
public static void main(String[] args) {
String ctimestamp = "1599648837833";
// 时间戳,用户自己生成
String cnonce = "123456";
// 6位随机码,用户自己生成
String requestParam = "";
//请求参数,form表单方式,鸿图大部分接口都是body传参,故这里大部分都是"",如果有参数,查看文档说明
String url = "/v1/api/device/list";
// 要请求的url,看要请求的接口说明
String method = "POST";
// 请求方式,看要请求的接口说明
String cappKey = "appkey1";
// appkey,固定,
String secret = "sdfajk3242324fa!djq7";
// 秘钥,固定
DeviceReq deviceReq = DeviceReq.builder().name("core").build();
// 构建请求体,看要请求的接口说明
// 计算签名
String csign = SignUtlis.sign(ctimestamp, cnonce, deviceReq, requestParam, url, method, cappKey, secret);
// 发起请求
String realUrl = "http://10.122.101.181:18082" + url;
postJson(ctimestamp, cnonce, cappKey, csign, deviceReq, realUrl);
}
private static void postJson(String ctimestamp, String cnonce, String cappKey, String csign, Object reqeustData, String url) {
String bodyJson = JSONObject.toJSONString(reqeustData);
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(3000, TimeUnit.SECONDS)
.readTimeout(30000, TimeUnit.SECONDS)
.writeTimeout(30000, TimeUnit.SECONDS)
.build();
MediaType mediaType = MediaType.parse("application/json;charset=UTF-8");
RequestBody requestBody = RequestBody.create(mediaType, bodyJson);
Request request = new Request.Builder()
.url(url)
.addHeader("cnonce", cnonce)
.addHeader("ctimestamp", ctimestamp)
.addHeader("cappKey", cappKey)
.addHeader("csign", csign)
.post(requestBody)
.build();
Call call = httpClient.newCall(request);
try {
Response response = call.execute();
System.out.println("请求结果:" + response.body().string());
System.out.println(response);
}
catch (Exception e) {
e.printStackTrace();
}
}
}