安全接入方式说明
为保证API调用过程的安全可靠,e签宝开放平台openapi统一采用HTTPS传输协议,并提供了两种安全接入鉴权方式,开发者可根据自身情况任选一种方式。
方式一:OAuth2.0鉴权(不推荐使用)
OAuth2.0鉴权方式,采用标准的OAuth2.0 Client Credentials方式进行认证鉴权。
方式二:请求签名鉴权(优先推荐)
请求签名鉴权方式,采用对请求数据进行签名的方式实现安全接入,防止请求过程数据被篡改或盗用。
请求签名鉴权前准备
第三方应用申请采用请求签名鉴权方式之前,需要先登录e签宝开放平台创建应用,应用创建成功后可以获取到身份识别信息:应用 ID(APP ID)和应用密钥(APP KEY)。
电子签名 SaaS API 标准版其他接口调用方式
安全接入方式选择请求签名鉴权方式时,每次调用电子签名 SaaS API 标准版中其他接口时无需在Header请求头中携带“X-Tsign-Open-Token”,传入授权码Token。
而是需要在调用API前,需要拼接签名字符串,并将签名计算后的字符串放在请求的 Header 传入,e签宝网关会通过Request请求中Header请求头中的X-Tsign-Open-Ca-Signature参数来获取签名值,然后通过计算签名来验证请求者的身份。
请求签名鉴权方式-请求头
安全接入选择请求签名鉴权方式,其请求头格式如下:
参数名称 | 类型 | 必选 | 参数说明 |
X-Tsign-Open-App-Id | string | 是 | 应用 AppId |
Content-Type | string | 是 | application/json; charset=UTF-8 |
X-Tsign-Open-Ca-Timestamp | string | 是 | API 调用者传递时间戳,值为当前时间的毫秒数,也就是从1970年1月1日起至今的时间转换为毫秒,时间戳有效时间为15分钟,为了防重放攻击。 字符串类型,仅限传入毫秒数 |
Accept | string | 是 | 建议统一填写 */* |
X-Tsign-Open-Ca-Signature | string | 是 | 签名字符串 |
Content-MD5 | string | 是 | 当请求 Body 非 Form 表单时,可以计算 Body 的 MD5 值传递给云网关进行 Body MD5 校验。建议当请求 Body 非 Form 表单时,加上此请求头。 |
X-Tsign-Open-Auth-Mode | string | 是 | 选择请求方式进行鉴权,固定值,Signature |
PostMan中示例如下:
请求签名鉴权具体方法
1、签名字符串生成
步骤一:待签字符串准备
待签字符串由以下字段拼接组成
参数 | 说明 |
HTTPMethod | 全大写,例如POST |
Accept | 希望服务器响应发送回来的是json格式的内容,例如:*/* 或 application/json。 |
Content-MD5 | Content-MD5 是指 Body 的 MD5 值,只有当 Body 非 Form 表单时才计算 MD5 |
Content-Type | 请求的与实体对应的MIME信息,例如:application/json;charset=UTF-8 |
Date | 可空,Date头域表示消息发送的时间,缓存在评估响应的新鲜度时要用到,时间的描述格式由RFC822定义。例如,Date: Thu, 11 Jul 2015 15:33:24 GMT。 |
Headers | 无需对headers进行签名,建议传空值 |
Url | url ,对请求的url进行签名(域名无需参与签名), 例如:/v2/identity/auth/web/indivAuthUrl,生成方法见下方 |
(1)待签字符串的拼接方法
String stringToSign= HTTPMethod + "\n" + Accept + "\n" + Content-MD5 + "\n" Content-Type + "\n" + Date + "\n" + Headers + Url
其中HTTPMethod为全大写,Accept、Content-MD5、Content-Type、Date 如果为空也需要添加换行符”\n”,Headers如果为空不需要添加”\n”。
注:当请求body为空时,必须设置Content-MD5="",否则接口鉴权会报错"code":401,"message":"INVALID_SIGNATURE"
(2)Content-MD5的计算方法
Content-MD5 是指 Body 的 MD5 值,只有当 Body 非 Form 表单时才计算 MD5。
计算方式为:
/*** * * @param str 待计算的消息 * @return MD5计算后摘要值的Base64编码(ContentMD5) * @throws Exception 加密过程中的异常信息 */ public String doContentMD5(String str) throws Exception { byte[] md5Bytes = null; MessageDigest md5 = null; String contentMD5 = null; try { md5 = MessageDigest.getInstance("MD5"); // 计算md5函数 md5.update(str.getBytes("UTF-8")); // 获取文件MD5的二进制数组(128位) md5Bytes = md5.digest(); // 把MD5摘要后的二进制数组md5Bytes使用Base64进行编码(而不是对32位的16进制字符串进行编码) contentMD5 = new String(Base64.encodeBase64(md5Bytes), "UTF-8"); } catch (NoSuchAlgorithmException e) { String msg = MessageFormat.format("不支持此算法: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } catch (UnsupportedEncodingException e) { String msg = MessageFormat.format("不支持的字符编码: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } return contentMD5; }
(3)Headers的添加方法
Headers 是指参与 Headers 签名计算的 Header 的 Key、Value 拼接的字符串,建议对 X-Tsign 开头以及自定义 Header 计算签名。若无需对Headers进行签名,可以设为空。
先对参与 Headers 签名计算的 Header的Key 按照字典排序后使用如下方式拼接,如果某个 Header 的 Value 为空,则使用 HeaderKey + “:” + “\n”参与签名,需要保留 Key 和英文冒号。
String headers = HeaderKey1 + ":" + HeaderValue1 + "\n"\+ HeaderKey2 + ":" + HeaderValue2 + "\n"\+ ... HeaderKeyN + ":" + HeaderValueN + "\n"
若添加了Headers,则需要将 Headers 签名中 Header 的 Key 使用英文逗号分割放到 Request 的 Header 中,Key为:X-Tsign-open-Ca-Signature-Headers。
(4)Url的添加方法
Url 指 Path + Query + Body 中 Form 参数,组织方法:对 Query+Form 参数按照字典对 Key 进行排序后按照如下方法拼接,如果 Query 或 Form 参数为空,则 Url = Path,不需要添加 ?,如果某个参数的 Value 为空只保留 Key 参与签名,等号不需要再加入签名。
String url = Path + "?" + Key1 + "=" + Value1 + "&" + Key2 + "=" + Value2 + ... "&" + KeyN + "=" + ValueN
注意:
1、如果Query和Form中存在同样的Key,则使用Form中的Key 参与签名计算。
2、Query参数的Value如果存在中文,则需要对Value值进行UrlEncode编码,否则会出现中文乱码或签名报错等情况。
步骤二:计算请求签名值
使用应用密钥(APP KEY)对待签名字符串采用HmacSHA256算法进行签名运算,从而得到签名字符串。
计算方法如下:
/*** * 计算请求签名值 * * @param message 待签名字符串 * @param secret 密钥APP KEY * @return HmacSHA256计算后摘要值的Base64编码 * @throws Exception 加密过程中的异常信息 */ public String doSignatureBase64(String message, String secret) throws Exception { String algorithm = "HmacSHA256"; Mac hmacSha256; String digestBase64 = null; try { hmacSha256 = Mac.getInstance(algorithm); byte[] keyBytes = secret.getBytes("UTF-8"); byte[] messageBytes = message.getBytes("UTF-8"); hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, algorithm)); // 使用HmacSHA256对二进制数据消息Bytes计算摘要 byte[] digestBytes = hmacSha256.doFinal(messageBytes); // 把摘要后的结果digestBytes使用Base64进行编码 digestBase64 = new String(Base64.encodeBase64(digestBytes), "UTF-8"); } catch (NoSuchAlgorithmException e) { String msg = MessageFormat.format("不支持此算法: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } catch (UnsupportedEncodingException e) { String msg = MessageFormat.format("不支持的字符编码: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } catch (InvalidKeyException e) { String msg = MessageFormat.format("无效的密钥规范: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } return digestBase64; }
2、请求签名鉴权示例代码
Test类
package esign.gateway.demo; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; import java.util.HashMap; import java.util.LinkedHashMap; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import net.sf.json.JSONObject; public class Test { public static void main(String[] args) { // 应用ID String appId = "4438XX6"; // 应用密钥 String appKey = "73935c284XX0c691f03"; // 接口调用域名 String host = "https://smlopenapi.esign.cn"; // 个人账户ID String accountId = "4c66c987eXXXc2eb474b5abc54"; // 请求签名鉴权-POST请求 testPost(appId, appKey, host); // 请求签名鉴权-GET请求 testGet(appId, appKey, host, accountId); } /*** * 请求签名鉴权-POST请求 * * @param appId * @param appKey * @param host */ public static void testPost(String appId, String appKey, String host) { // 个人创建账号接口地址 String accountsApi = "/v1/accounts/createByThirdPartyUserId"; // 个人创建账号接口请求地址 String accountsApiUrl = host + accountsApi; try { // 构建请求Body体 JSONObject reqBodyObj = new JSONObject(); reqBodyObj.put("thirdPartyUserId", "229"); reqBodyObj.put("name", "张三"); reqBodyObj.put("idType", "CRED_PSN_CH_IDCARD"); reqBodyObj.put("idNumber", "330621"); reqBodyObj.put("mobile", "152****4800"); reqBodyObj.put("email", "152****800@163.com"); // 请求Body体数据 String reqBodyData = reqBodyObj.toString(); // 对请求Body体内的数据计算ContentMD5 String contentMD5 = doContentMD5(reqBodyData); // 构建待签名字符串 String method = "POST"; String accept = "*/*"; String contentType = "application/json; charset=UTF-8"; String url = accountsApi; String date = ""; String headers = ""; StringBuffer sb = new StringBuffer(); sb.append(method).append("\n").append(accept).append("\n").append(contentMD5).append("\n") .append(contentType).append("\n").append(date).append("\n"); if ("".equals(headers)) { sb.append(headers).append(url); } else { sb.append(headers).append("\n").append(url); } // 构建参与请求签名计算的明文 String plaintext = sb.toString(); // 计算请求签名值 String reqSignature = doSignatureBase64(plaintext, appKey); // 获取时间戳(精确到毫秒) long timeStamp = timeStamp(); // 构建请求头 LinkedHashMap<String, String> header = new LinkedHashMap<String, String>(); header.put("X-Tsign-Open-App-Id", appId); header.put("X-Tsign-Open-Auth-Mode", "Signature"); header.put("X-Tsign-Open-Ca-Timestamp", String.valueOf(timeStamp)); header.put("Accept", accept); header.put("Content-Type", contentType); header.put("X-Tsign-Open-Ca-Signature", reqSignature); header.put("Content-MD5", contentMD5); // 发送POST请求 String result = HTTPHelper.sendPOST(accountsApiUrl, reqBodyData, header, "UTF-8"); JSONObject resultObj = JSONObject.fromObject(result); System.out.println("请求返回信息: " + resultObj.toString()); } catch (Exception e) { e.printStackTrace(); String msg = MessageFormat.format("请求签名鉴权方式调用接口出现异常: {0}", e.getMessage()); System.out.println(msg); } } /*** * 请求签名鉴权-GET请求 * * @param appId * @param appKey * @param host * @param flowId */ public static void testGet(String appId, String appKey, String host, String flowId) { // 签署流程查询API地址 String getSignFlowApi = "/v1/signflows/" + flowId; // 签署流程查询接口请求地址 String hostGetSignFlowApi = host + getSignFlowApi; try { // GET请求时ContentMD5为"" String contentMD5 = ""; // 构建待签名字符串 String method = "GET"; String accept = "*/*"; String contentType = "application/json; charset=UTF-8"; String url = getSignFlowApi; String date = ""; String headers = ""; StringBuffer sb = new StringBuffer(); sb.append(method).append("\n").append(accept).append("\n").append(contentMD5).append("\n") .append(contentType).append("\n").append(date).append("\n"); if ("".equals(headers)) { sb.append(headers).append(url); } else { sb.append(headers).append("\n").append(url); } // 构建参与请求签名计算的明文 String plaintext = sb.toString(); // 计算请求签名值 String reqSignature = doSignatureBase64(plaintext, appKey); // 获取时间戳(精确到毫秒) long timeStamp = timeStamp(); // 构建请求头 LinkedHashMap<String, String> header = new LinkedHashMap<String, String>(); header.put("X-Tsign-Open-App-Id", appId); header.put("X-Tsign-Open-Auth-Mode", "Signature"); header.put("X-Tsign-Open-Ca-Timestamp", String.valueOf(timeStamp)); header.put("Accept", accept); header.put("Content-Type", contentType); header.put("X-Tsign-Open-Ca-Signature", reqSignature); header.put("Content-MD5", contentMD5); // 发送GET请求 String result = HTTPHelper.sendGet(hostGetSignFlowApi, header, "UTF-8"); JSONObject resultObj = JSONObject.fromObject(result); System.out.println("请求返回信息: " + resultObj.toString()); } catch (Exception e) { e.printStackTrace(); String msg = MessageFormat.format("请求签名鉴权方式调用接口出现异常: {0}", e.getMessage()); System.out.println(msg); } } /*** * * @param str 待计算的消息 * @return MD5计算后摘要值的Base64编码(ContentMD5) * @throws Exception 加密过程中的异常信息 */ public static String doContentMD5(String str) throws Exception { byte[] md5Bytes = null; MessageDigest md5 = null; String contentMD5 = null; try { md5 = MessageDigest.getInstance("MD5"); // 计算md5函数 md5.update(str.getBytes("UTF-8")); // 获取文件MD5的二进制数组(128位) md5Bytes = md5.digest(); // 把MD5摘要后的二进制数组md5Bytes使用Base64进行编码(而不是对32位的16进制字符串进行编码) contentMD5 = new String(Base64.encodeBase64(md5Bytes), "UTF-8"); } catch (NoSuchAlgorithmException e) { String msg = MessageFormat.format("不支持此算法: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } catch (UnsupportedEncodingException e) { String msg = MessageFormat.format("不支持的字符编码: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } return contentMD5; } /*** * 计算请求签名值 * * @param message 待计算的消息 * @param secret 密钥 * @return HmacSHA256计算后摘要值的Base64编码 * @throws Exception 加密过程中的异常信息 */ public static String doSignatureBase64(String message, String secret) throws Exception { String algorithm = "HmacSHA256"; Mac hmacSha256; String digestBase64 = null; try { hmacSha256 = Mac.getInstance(algorithm); byte[] keyBytes = secret.getBytes("UTF-8"); byte[] messageBytes = message.getBytes("UTF-8"); hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, algorithm)); // 使用HmacSHA256对二进制数据消息Bytes计算摘要 byte[] digestBytes = hmacSha256.doFinal(messageBytes); // 把摘要后的结果digestBytes转换成十六进制的字符串 // String digestBase64 = Hex.encodeHexString(digestBytes); // 把摘要后的结果digestBytes使用Base64进行编码 digestBase64 = new String(Base64.encodeBase64(digestBytes), "UTF-8"); } catch (NoSuchAlgorithmException e) { String msg = MessageFormat.format("不支持此算法: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } catch (UnsupportedEncodingException e) { String msg = MessageFormat.format("不支持的字符编码: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } catch (InvalidKeyException e) { String msg = MessageFormat.format("无效的密钥规范: {0}", e.getMessage()); Exception ex = new Exception(msg); ex.initCause(e); throw ex; } return digestBase64; } /*** * 获取时间戳(毫秒级) * * @return 毫秒级时间戳,如 1578446909000 */ public static long timeStamp() { long timeStamp = System.currentTimeMillis(); return timeStamp; } }
HTTPHelper辅助类
package esign.gateway.demo; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HTTPHelper { // slf4j日志记录器 private static final Logger LOG = LoggerFactory.getLogger(HTTPHelper.class); /*** * 向指定URL发送GET方法的请求 * * @param apiUrl * @param data * @param projectId * @param signature * @param encoding * @return * @throws Exception */ public static String sendGet(String apiUrl, LinkedHashMap<String, String> headers, String encoding) throws Exception { // 获得响应内容 String http_RespContent = null; HttpURLConnection httpURLConnection = null; int http_StatusCode = 0; String http_RespMessage = null; try { LOG.info(">>>> 实际请求Url: " + apiUrl); // 建立连接 URL url = new URL(apiUrl); httpURLConnection = (HttpURLConnection) url.openConnection(); // 需要输出 httpURLConnection.setDoOutput(true); // 需要输入 httpURLConnection.setDoInput(true); // 不允许缓存 httpURLConnection.setUseCaches(false); // HTTP请求方式 httpURLConnection.setRequestMethod("GET"); // 设置Headers if (null != headers) { for (String key : headers.keySet()) { httpURLConnection.setRequestProperty(key, headers.get(key)); } } // 连接会话 httpURLConnection.connect(); // 获得响应状态(HTTP状态码) http_StatusCode = httpURLConnection.getResponseCode(); // 获得响应消息(HTTP状态码描述) http_RespMessage = httpURLConnection.getResponseMessage(); // 获得响应内容 if (HttpURLConnection.HTTP_OK == http_StatusCode) { // 返回响应结果 http_RespContent = getResponseContent(httpURLConnection); } else { // 返回非200状态时响应结果 http_RespContent = getErrorResponseContent(httpURLConnection); String msg = MessageFormat.format("请求失败: Http状态码 = {0} , {1}", http_StatusCode, http_RespMessage); LOG.info(msg); } } catch (UnknownHostException e) { String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage()); Exception ex = new Exception(message); ex.initCause(e); throw ex; } catch (MalformedURLException e) { String message = MessageFormat.format("格式错误的URL: {0}", e.getMessage()); Exception ex = new Exception(message); ex.initCause(e); throw ex; } catch (IOException e) { String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage()); Exception ex = new Exception(message); ex.initCause(e); throw ex; } catch (Exception e) { String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage()); Exception ex = new Exception(message); ex.initCause(e); throw ex; } finally { if (null != httpURLConnection) { httpURLConnection.disconnect(); } } return http_RespContent; } /*** * 向指定URL发送POST方法的请求 * * @param apiUrl * @param data * @param projectId * @param signature * @param encoding * @return * @throws Exception */ public static String sendPOST(String apiUrl, String data, LinkedHashMap<String, String> headers, String encoding) throws Exception { // 获得响应内容 String http_RespContent = null; HttpURLConnection httpURLConnection = null; int http_StatusCode = 0; String http_RespMessage = null; try { // 建立连接 URL url = new URL(apiUrl); httpURLConnection = (HttpURLConnection) url.openConnection(); // 需要输出 httpURLConnection.setDoOutput(true); // 需要输入 httpURLConnection.setDoInput(true); // 不允许缓存 httpURLConnection.setUseCaches(false); // HTTP请求方式 httpURLConnection.setRequestMethod("POST"); // 设置Headers if (null != headers) { for (String key : headers.keySet()) { httpURLConnection.setRequestProperty(key, headers.get(key)); } } // 连接会话 httpURLConnection.connect(); // 建立输入流,向指向的URL传入参数 DataOutputStream dos = new DataOutputStream(httpURLConnection.getOutputStream()); // 设置请求参数 dos.write(data.getBytes(encoding)); dos.flush(); dos.close(); // 获得响应状态(HTTP状态码) http_StatusCode = httpURLConnection.getResponseCode(); // 获得响应消息(HTTP状态码描述) http_RespMessage = httpURLConnection.getResponseMessage(); // 获得响应内容 if (HttpURLConnection.HTTP_OK == http_StatusCode) { // 返回响应结果 http_RespContent = getResponseContent(httpURLConnection); } else { // 返回非200状态时响应结果 http_RespContent = getErrorResponseContent(httpURLConnection); String msg = MessageFormat.format("请求失败: Http状态码 = {0} , {1}", http_StatusCode, http_RespMessage); LOG.info(msg); } } catch (UnknownHostException e) { String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage()); Exception ex = new Exception(message); ex.initCause(e); throw ex; } catch (MalformedURLException e) { String message = MessageFormat.format("格式错误的URL: {0}", e.getMessage()); Exception ex = new Exception(message); ex.initCause(e); throw ex; } catch (IOException e) { String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage()); Exception ex = new Exception(message); ex.initCause(e); throw ex; } catch (Exception e) { String message = MessageFormat.format("网络请求时发生异常: {0}", e.getMessage()); Exception ex = new Exception(message); ex.initCause(e); throw ex; } finally { if (null != httpURLConnection) { httpURLConnection.disconnect(); } } return http_RespContent; } /*** * 读取HttpResponse响应内容 * * @param httpURLConnection * @return * @throws UnsupportedEncodingException * @throws IOException */ private static String getResponseContent(HttpURLConnection httpURLConnection) throws UnsupportedEncodingException, IOException { StringBuffer contentBuffer = null; BufferedReader responseReader = null; try { contentBuffer = new StringBuffer(); String readLine = null; responseReader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), "UTF-8")); while ((readLine = responseReader.readLine()) != null) { contentBuffer.append(readLine); } } finally { if (null != responseReader) { responseReader.close(); } } return contentBuffer.toString(); } /*** * 读取HttpResponse响应内容 * * @param httpURLConnection * @return * @throws UnsupportedEncodingException * @throws IOException */ private static String getErrorResponseContent(HttpURLConnection httpURLConnection) throws UnsupportedEncodingException, IOException { StringBuffer contentBuffer = null; BufferedReader responseReader = null; try { contentBuffer = new StringBuffer(); String readLine = null; responseReader = new BufferedReader(new InputStreamReader(httpURLConnection.getErrorStream(), "UTF-8")); while ((readLine = responseReader.readLine()) != null) { contentBuffer.append(readLine); } } finally { if (null != responseReader) { responseReader.close(); } } return contentBuffer.toString(); } }