2022年11月7日,国家卫健委、国家中医药局、国家疾控局联合下发了《关于印发“十四五"全民健康信息化规划的通知》(国卫规划发〔2022〕30号),在《国家“十四五”全民健康信息化规划》中明确指出“拓展电子证照应用领域和证照免提交范围,推动全国互通互认”、“鼓励应用区块链技术加强身份标识管理,丰富区块链的应用场景”、“构建卫生健康行业网络可信体系。
2021年11月17日,四川省人民政府办公厅关于印发《四川省“十四五”卫生健康发展规划》的通知(川办发〔2021〕65号),在《四川省“十四五”卫生健康发展规划》中指出“注重以需求为导向,以应用为引领,以安全为底线,推进卫生健康信息化纵深发展。 强化信息化支撑体系。 推进 5G、云计算、大数据、物联网、人工智能、区块链等新兴信息技术在卫生健康行业融合发展,夯实数字健康发展基础”、“到 2025 年,力争 60% 的三级公立医疗机构建成三星智慧医院、20% 的二级公立医疗机构建成二星智慧医院;市级及以上综合医院电子病历系统应用水平分级评价达到 5 级以上水平,县级公立综合医院达到 4 级水平。”、“加强网络信息安全保护。深化国产密码应用”、“加快网络可信体系建设,逐步实现患者身份在线核验及医疗机构、医师、护士等信息公众查询”。
2023年7月17日,四川省卫生健康委员会下发关于印发《四川省卫生健康信息化三年行动计划(2023-2025年)》的通知(川卫发〔2023〕7号),在《四川省卫生健康信息化三年行动计划(2023-2025年)》中指出“发挥标准的规范、引领和支撑作用”、“加强身份认证、电子印章等基础支撑,探索建立基于区块链技术的多因素行业身份认证体系”、“统筹推进电子证照建设应用”、“构建标准统一的服务能力开放体系,通过省级共建共享的基层应用生态补齐基层信息化短板”、“逐步推广卫生健康行业商用密码应用”。
2023年4月,四川省卫生健康委员会办公室发布了《关于开展2023年度智慧医院评价工作的通知》,在《四川省智慧医院评价标准》“五、新兴技术应用——5.5区块链应用”提出“基于区块链技术实现医护人员、医疗机构电子执业证照存证上链,支持执业主体实现在线认证和亮证执业、互联网医疗服务、跨机构电子病历协同共享等应用”。
为此,四川省卫生健康信息中心建设了四川省卫生健康基础资源共享链,基础资源共享链实现了全省执业主体身份为基础的区块链试点应用服务网络,通过提供存证上链、执业主体注册登记领证(执业机构和个人)、亮证、查证和认证服务,以及其它如基于执业主体身份的签名等扩展服务,构建以基础资源链为纽带的可信应用生态,推广区块链技术在行业的深化应用。
为了落实《四川省卫生健康信息化三年行动计划(2023-2025年)》,满足智慧医院建设需求,按照《电子签名法》《电子证照 共享服务接口规范》《基于云计算的电子签名服务技术要求》《卫生系统电子认证服务规范》《卫生系统数字证书应用集成规范》《卫生系统数字证书格式规范》等相关规定,融合电子证照与数字证书在执业主体身份认证方面的优势,结合公安的人脸实名认证等多种认证手段,我们提供了“跨CA机构多证照身份认证服务”,并且编制了相关的接入指南和标准文件,来支撑各医疗机构接入四川省卫生健康基础资源共享链,满足“医疗机构电子执业证照存证上链,支持执业主体实现在线认证和亮证执业”等智慧医院评价要求,能够为医疗行业提供多身份认证服务,解决医疗行业执业主体身份认证的相关问题和电子证照推广使用中的领证、亮证、查证和认证等相关电子证照应用的相关问题,解决医疗机构内部电子签名应用等问题,规范医疗行业执业主体电子证照的应用及CA认证服务的应用。
目标
本服务聚焦在跨CA机构多证照身份认证方面,满足各医疗机构使用基础资源链的“跨CA机构多证照身份认证服务”,此服务实现了执业主体注册登记领证(执业机构和个人)、亮证、查证和认证服务,以及其它如基于执业主体身份的电子签名等扩展服务,能够满足各医疗机构智慧医院评审需求,方案主要目标如下:
- 推动电子证照应用
通过接入“跨CA机构多证照身份认证服务”,能够推动执业主体从领证、发证、亮证、查证等应用,推动电子证照在医疗机构各种服务场景的应用,包括但不限于互联网医疗执业人员的证照亮证应用,实体医院的电子屏亮证、执业主体基于证照的登录认证、实体医院内应用系统(LIS/HIS/PACS等)内基于电子证照的电子签名,医院网站的机构执业证的亮证等。
- 融合CA认证能力实现医疗机构内电子签名应用
通过接入“跨CA机构多证照身份认证服务”,能够满足医疗机构建立统一的电子认证服务体系和业务应用安全支撑体系的要求,支撑电子病历系统、HIS系统、LIS系统、PACS系统对CA认证服务的对接,满足医生、护士、医技人员对电子签名的使用需求。
- 支撑医疗机构满足智慧医院评审要求
通过接入“跨CA机构多证照身份认证服务”,可以支撑智慧医院评价中 “医疗机构电子执业证照存证上链,支持执业主体实现在线认证和亮证执业” 的要求。
整体架构

分布式身份认证集成中台可以部署在医疗机构内部、地方卫健委,同时也提供了云服务的模式来为医疗机构提供服务。分布式身份认证集成中台为机构的应用系统提供API接口服务,其中包括身份认证服务、电子签章服务。身份认证服务为医疗机构的应用系统提供电子证照、基于电子证照的电子签名、签名验证、及时间戳等基础服务;电子签章服务为医疗机构的应用系统提供基于数字证书的电子签章服务,以确保电子文档来源真实性以及文档的完整性,防止对文档未经授权的篡改,并确保签章行为的不可否认性。所有的电子签名数据都在分布式身份认证集成中台中安全存管,不会通过互联网进行传递。
分布式认证管理后台主要管理相关的配置信息、电子证照信息、数字证书等的相关配置信息,包括配置信息的下发,数字证书、电子证照信息的下发等,不会与医疗机构的应用系统有任何的交互。
执业人员需要使用认证APP完成执业主体身份的认证,电子证照的领取、出示,以及基于电子证照的电子签名等。
CA认证机构遵循标准接入平台后,医疗机构可以在CA机构中任意选择提供CA认证服务的CA机构,平台实现了对CA机构接入的标准化,以及对医疗机构提供电子签名服务的标准化。
平台打通了执业主体身份的认证源,能够实现医疗主体的身份认证,以及电子证照的发放。
部署模式

医疗机构应用只需要与分布式身份认证集成中台进行对接即可实现与四川省卫生健康基础资源共享链中的电子证照、CA认证等服务的集成,满足智慧医院评价标准要求,主要部署模式包括:
- 部署模式1:医疗机构本地部署模式
分布式身份认证集成中台部署在医疗机构内网,相应的医疗系统(LIS、HIS、PACS、EMR…)通过内网调用分布式身份认证集成中台提供的接口服务,从而完成四川省卫生健康基础资源共享链的身份认证的对接。
对于执业主体(医生、护士等)则需要下载认证APP,领取执业主体的电子证照(电子执业证),并且可以通过认证APP实现医疗数据的电子签名。
在如下条件下建议医疗机构本地部署:
1)医疗机构的应用系统对安全性有严格要求,业务系统与外部系统对接不能通过互联网边界进行对接情况时。
2)医疗机构的应用系统使用电子签名,并且签名业务数据量较大时。
3)医疗机构有完善的医疗信息化系统,并且有自己的机房,能够提供硬件服务器或对性能要求非常高时。
- 部署模式2:地方卫健委集中部署模式
在地方卫健委集中部署分布式身份认证集成中台,各接入医院可以通过使用地方卫健委集成部署的中台服务,完成四川省卫生健康基础资源共享链的身份认证的对接。
- 部署模式3:云服务模式
为了支持各医疗机构智慧医院的评审工作,我们也提供了云服务模式,各接入医疗机构可以使用云服务模式的中台服务,完成四川省卫生健康基础资源共享链的身份认证的对接。
医疗机构应用系统接入
应用接入指南:
应用接入标准:
服务接口地址:
演示demo地址:
https://testmicrosrv.scca.com.cn:9702/#/login
扫码登录。
移动APP下载二维码地址:
[安卓版] https://www.pgyer.com/HWnM
app测试账户:18000000000
密码:a12345678[苹果版]在苹果App Store,搜索【易证宝】,下载并安装。
app测试账户:18000000000
密码:a12345678具体使用方法如下:
a)切换到测试环境
苹果手机连续点击红框区域,安卓手机长按蓝色框,出现如下界面:
点击【请选择环境】,进入如下界面。
选择红框内的测试环境。
b)输入测试账号、密码登录【账号:18000000000,密码:a12345678】
点击登录,并选择一个测试医疗机构,如下图:
即可登录测试。
测试账号信息
应用appid :1706139578761072641
应用appsecret: 4dccdeef7a1d4c75a24b3c1b30f93fa1app个人账号:18000000000
密码:a12345678单位唯一标识:1231414234
单位名称:四川ca测试医院001标准涉及工具包下载:
点击下载[接入工具包(sm3-tools)]
跨证书认证机构多证照身份认证服务签名值使用示例:
应用系统在与身份认证服务通讯时,应提供认证信息,用于鉴别其身份。可以通过验证签名的方式鉴别各个接入的应用系统的身份。
a)身份认证服务向申请接入的应用系统分配的唯一app_id和app_secret。
b)通过接口传输数据时,调应通过HMAC-SM3算法对请求数据进行加签计算。
c)开放接口采用https协议
公共参数放入请求头中进行传输,请求的Header中包含认证信息如下:参数名 参数类型 参数说明 app_id String 接入应用系统app_id,由身份认证服务统一分配。 signature String 参数签名值,由分配app_secret和请求参数计算得出结果,采用HEX编码 timestamp String 请求发送的时间戳(Unix Timestamp, 毫秒) nonce String 请求随机数,每笔业务在一定时间内唯一(2分钟) 一、签名值生成规则:
json提交
a)json字符串后面拼接随机数(nonce对应值,随机数在前)和时间戳(timestamp对应值)得到字符串
b) 将a得到的字符串进行HmacSM3运算,计算后将结果转换为16进制(小写),即为签名信息,放入请求头(signature)中
二、签名值算法
a) 签名值算法java代码参考(请参考接入工具包(sm3-tools)):
public class HttpUtils {private static final RestTemplate template = new RestTemplate();
public static void main(String[] args) {
Person person = new Person("1231414234","2");
postJson("{{基础地址}}/open/signature/sealQuerysealQue", JSONObject.toJSONString(person), new HashMap<>(), "scca分配给你的app_id", "scca分配给你的app_secret")
}
public static String postJson(String uri, String json, Map<String, String> headerParams, String appId, String secret) {
HttpHeaders header = getHeaders(headerParams, json, appId, secret);
header.add("Content-Type", "application/json;charset=UTF-8");
HttpEntity<String> entity = new HttpEntity<>(json, header);
return execute(uri, entity);
}
private static HttpHeaders getHeaders(Map<String, String> headerParams, String toSign, String appId, String secret) {
HttpHeaders headers = new HttpHeaders();
if (headerParams != null) {
headerParams.forEach(headers::add);
}
String nonce = nonce();
String ts = String.valueOf(System.currentTimeMillis());
String ori = toSign.concat(nonce).concat(ts);
System.out.println(ori);
System.out.println(secret);
String signature = signature(ori, secret);
headers.add("signature", signature);
headers.add("app_id", appId);
headers.add("nonce", nonce);
headers.add("timestamp", ts);
return headers;
}
private static String nonce() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replaceAll("[^0-9]", "");
}
private static String execute(String url, HttpEntity<?> entity) throws RuntimeException {
ResponseEntity<String> result = template.postForEntity(url, entity, String.class, new Object[0]);
if (result.getStatusCodeValue() == 200) {
return (String) result.getBody();
} else {
throw new RuntimeException("请求服务器出错");
}
}
private static String signature(String toSign, String secret) {
return calc(toSign, secret);
}
private static String calc(String toSign, String secret) {
try {
byte[] bytes = HMACUtils.hmacSM3(toSign.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8));
return HMACUtils.byteArrayToHexString(bytes);
} catch (Throwable var4) {
throw new RuntimeException("hmac签名失败.", var4);
}
}
}
public class HMACUtils {
public HMACUtils() {
}
public static byte[] hmacSM3(byte[] data, byte[] sm4Key) {
return hmac(data, sm4Key);
}
public static byte[] hmac(byte[] data, byte[] key) {
KeyParameter keyParameter = new KeyParameter(key);
SM3Digest sm3Digest = new SM3Digest();
HMac hMac = new HMac(sm3Digest);
hMac.init(keyParameter);
hMac.update(data, 0, data.length);
byte[] result = new byte[hMac.getMacSize()];
hMac.doFinal(result, 0);
return result;
}
private static final char[] hex = "0123456789abcdef".toCharArray();
public static String byteArrayToHexString(byte[] bytes) {
if (null == bytes) {
return null;
}
StringBuilder sb = new StringBuilder(bytes.length << 1);
for (int i = 0; i < bytes.length; ++i) {
sb.append(hex[(bytes[i] & 0xf0) >> 4])
.append(hex[(bytes[i] & 0x0f)]) ;
}
return sb.toString();
}
}
b) 签名值算法Apifox脚本如下:
// 变量名称定义(Header取值时会被转换成小写)
var secretVariableKey = "appSecret" // secret变量名称
var signatureVariableKey = "_signature" //计算出的签名变量名称
// 这个参数只做打印, 不参与计算
var appIdVariableKey = "appId" // appID变量名称/Header名称
// 随机数 & 时间戳处理
// 签名值计算使用的时间戳变量名称/Header名称, 不需要时设置为空串
var timestampVariableKey = "timestamp"
// 签名值计算使用的随机数变量名称/Header名称, 不需要时设置为空串
var nonceVariableKey = "nonce"
/*
内置变量替换在pre script之后执行, 这里要计算签名值, 需要自定义变量才能使用(变量以`_`打头表示生成~~~)
所有变量参考:
https://learning.postman.com/docs/writing-scripts/script-references/variables-list/
*/
pm.globals.set("_timestamp", pm.variables.replaceIn('{{$timestamp}}')) // 时间戳, 秒
pm.globals.set("_timestampMs", pm.variables.replaceIn('{{$timestamp}}') * 1000) // 时间戳, MS
pm.globals.set("_randomUUID", pm.variables.replaceIn('{{$randomUUID}}'))
pm.globals.set("_randomInt", pm.variables.replaceIn('{{$randomInt}}')) // 1000以内整数
pm.globals.set("_randomCreditCardMask", pm.variables.replaceIn('{{$randomCreditCardMask}}')) // 固定4位整数
pm.globals.set("_randomBankAccount", pm.variables.replaceIn('{{$randomBankAccount}}')) // 8位随机数
pm.globals.set("_nonce", pm.variables.replaceIn('{{$randomBankAccount}}')) // 8位随机数
pm.globals.set("_randomUserName", pm.variables.replaceIn('{{$randomUserName}}'))
pm.globals.set("_randomPassword", pm.variables.replaceIn('{{$randomPassword}}'))
pm.globals.set("_randomEmail", pm.variables.replaceIn('{{$randomEmail}}'))
pm.globals.set("_randomFirstName", pm.variables.replaceIn('{{$randomFirstName}}'))
pm.globals.set("_randomLastName", pm.variables.replaceIn('{{$randomLastName}}'))
pm.globals.set("_randomCompanyName", pm.variables.replaceIn('{{$randomCompanyName}}'))
// 不常用的
pm.globals.set("_randomFilePath", pm.variables.replaceIn('{{$randomFilePath}}')) // 文件路径
pm.globals.set("_randomMimeType", pm.variables.replaceIn('{{$randomMimeType}}')) //
pm.globals.set("_randomProduct", pm.variables.replaceIn('{{$randomProduct}}')) // 随机的产品
pm.globals.set("_randomDepartment", pm.variables.replaceIn('{{$randomDepartment}}')) //商业分类: Movies, Electronics
pm.globals.set("_randomLocale", pm.variables.replaceIn('{{$randomLocale}}')) // 地点, 两位简写
pm.globals.set("_randomTransactionType", pm.variables.replaceIn('{{$randomTransactionType}}')) // 交易类型
pm.globals.set("_randomNoun", pm.variables.replaceIn('{{$randomNoun}}')) // 随机的名词: bus, bandwidth
pm.globals.set("_randomLoremParagraphs", pm.variables.replaceIn('{{$randomLoremParagraphs}}')) // 文章段落
pm.globals.set("_randomUrl", pm.variables.replaceIn('{{$randomUrl}}')) // 随机网址
pm.globals.set("_randomAlphaNumeric", pm.variables.replaceIn('{{$randomAlphaNumeric}}')) // 随机字符
pm.globals.set("_randomColor", pm.variables.replaceIn('{{$randomColor}}')) //颜色: red, blue
pm.globals.set("_randomHexColor", pm.variables.replaceIn('{{$randomHexColor}}')) // 随机Hex颜色: #47594a
pm.globals.set("_randomWeekday", pm.variables.replaceIn('{{$randomWeekday}}')) // 星期(英文)
// ============================ 开始处理签名计算
// ============================ 开始处理签名计算
// ============================ 开始处理签名计算
var keys = Object.keys(request.data), i, len = keys.length;
var requestBody = "";
// 从变量中提取值的工具函数
var replaceBodyVar = function(val){
var holder = val.match(/\{\{(\w+?)}}/g)
if(holder && holder.length){
for(var i = 0; i < holder.length; i ++){
var k = holder[i];
// 需要使用在上面代码中定义了的变量才能正常工作
var v = pm.globals.get(k.replace(/({|})/g, ''))
val = val.replace(k, v)
console.log("替换变量值: >>>> "+k+" -> "+ v)
}
console.log("替换处理后的请求参数:", requestBody)
}
return val;
}
// 替换header中的变量名称, 可能是header.key或者是环境变量中的key
var replaceHeaderVar = function(variableKey) {
var key;
var val = request.headers[variableKey]
if(/\{\{.*?\}\}/g.test(val)){
// 如果是header.key, 那么值应该是变量, 提取表达式
key = val.replace(/({|})/g, '')
} else{
// 如果为env.key, 直接获取
key = variableKey;
}
var v = pm.environment.get(key) || pm.globals.get(key)
console.log("replace header var, key = " + val + "; val = " + v)
return v
}
// 处理Secret
var secret = pm.environment.get(secretVariableKey) || pm.globals.get(secretVariableKey)
if(!secret){
throw new Error("环境变量中未配置Secret信息, secret Key : " + secretVariableKey)
}
// 处理APPID
// console.log(request.headers)
var appId = replaceHeaderVar(appIdVariableKey)
// if(!appId){
// throw new Error("Header和env中都找不到AppID信息, AppID Key : " + appIdVariableKey)
// }
// 开始计算签名值
console.log(request)
var contentType = request.headers['content-type'] || "application/x-www-form-urlencoded"
if(contentType === 'application/json'){
//ch请求json 去空格换行,保证参数计算和服务端拿到的一致
dataBody = request.data.replace(/[ ]|[\r\n]/g,"")
pm.request.body.update(dataBody)
requestBody = replaceBodyVar(request.data)
} else{
keys.sort(); //根据key经行排序
for (var index in keys) {
var param = replaceBodyVar(request.data[keys[index]])
// formdata hack: 参数为文件时, 值为空字符串
var notEmpty = param && param.length > 0;
requestBody += notEmpty ? param : '';
}
//requestBody=requestBody.substr(0, requestBody.length-1)
}
if(nonceVariableKey){
requestBody = requestBody + replaceHeaderVar(nonceVariableKey)
}
if(timestampVariableKey){
requestBody = requestBody + replaceHeaderVar(timestampVariableKey)
}
console.log("cal sig("+contentType+") using app.secret: [" + secret + "] for AppId: [" + appId + "]");
//ch计算签名参数 去空格换行,保证参数计算和服务端拿到的一致
requestBody = requestBody.replace(/[ ]|[\r\n]/g,"")
console.log("request body(签名原文22222):"+requestBody);
var signHmacSM3
fox.liveRequire("sm-crypto", (smObj) => {
try {
let secretBytes = stringToByte(secret)
signHmacSM3 = smObj.sm3(requestBody,{key:secretBytes})
console.log("sm3=",signHmacSM3)
pm.globals.set(signatureVariableKey, signHmacSM3);
} catch (error) {
console.error("An error occurred during liveRequire callback", error);
throw error;
}
});
//ch字符串转字节
function stringToByte (str) {
var len, c;
len = str.length;
var bytes = [];
for (var i = 0; i < len; i++) {
c = str.charCodeAt(i);
if (c >= 0x010000 && c <= 0x10FFFF) {
bytes.push(((c >> 18) & 0x07) | 0xF0);
bytes.push(((c >> 12) & 0x3F) | 0x80);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if (c >= 0x000800 && c <= 0x00FFFF) {
bytes.push(((c >> 12) & 0x0F) | 0xE0);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if (c >= 0x000080 && c <= 0x0007FF) {
bytes.push(((c >> 6) & 0x1F) | 0xC0);
bytes.push((c & 0x3F) | 0x80);
} else {
bytes.push(c & 0xFF);
}
}
return bytes;
}
CA机构及证照机构接入
- 应用接入指南:
《电子证照及CA机构身份认证服务接入指南》 - 认证机构接入标准:
团体标准-《认证服务机构接入接口规范》
围绕四川省医疗卫生健康业务、服务应用场景,搭建“身份认证的基础平台”(以下简称身份认证平台),推动以医护人员、医疗机构电子执业证照存证上链为牵引区块链技术应用生态建设。
- 构建基于电子证照的执业身份认证服务,集成CA身份认证
- 实现基于执业主体可信身份的互联网医疗服务认证、亮证及监管
- 支持执业主体实现在线资格认证和CA认证的集成服务和应用
项目架构
身份认证服务为机构的应用系统提供API接口服务,其中包括身份认证服务、电子签章服务。
身份认证服务为机构的应用系统提供基于数字证书的电子签名、签名验证、时间戳、及电子证照等基础服务。
电子签章服务为机构的应用系统提供基于数字证书的电子签章服务,以确保电子文档来源真实性以及文档的完整性,防止对文档未经授权的篡改,并确保签章行为的不可否认性。
认证服务机构可以遵循接入标准的定义接入身份认证服务。认证服务机构包括:证书认证机构、电子证照认证机构。
医疗机构应用系统接入
应用接入指南:
应用接入标准:
服务接口地址:
演示demo地址:
移动APP下载二维码地址:
[安卓版] https://www.pgyer.com/HWnM
app测试账户:18782983817或18000000000
密码:a12345678测试账号信息
应用appid :1706139578761072641
应用appsecret: 4dccdeef7a1d4c75a24b3c1b30f93fa1app个人账号:18000000000
密码:a12345678单位唯一标识:1231414234
单位名称:四川ca测试医院001标准涉及工具包下载:
点击下载[接入工具包(sm3-tools)]
跨证书认证机构多证照身份认证服务签名值使用示例:
应用系统在与身份认证服务通讯时,应提供认证信息,用于鉴别其身份。可以通过验证签名的方式鉴别各个接入的应用系统的身份。
a)身份认证服务向申请接入的应用系统分配的唯一app_id和app_secret。
b)通过接口传输数据时,调应通过HMAC-SM3算法对请求数据进行加签计算。
c)开放接口采用https协议
公共参数放入请求头中进行传输,请求的Header中包含认证信息如下:参数名 参数类型 参数说明 app_id String 接入应用系统app_id,由身份认证服务统一分配。 signature String 参数签名值,由分配app_secret和请求参数计算得出结果,采用HEX编码 timestamp String 请求发送的时间戳(Unix Timestamp, 毫秒) nonce String 请求随机数,每笔业务在一定时间内唯一(2分钟) 一、签名值生成规则:
json提交
a)json字符串后面拼接随机数(nonce对应值,随机数在前)和时间戳(timestamp对应值)得到字符串
b) 将a得到的字符串进行HmacSM3运算,计算后将结果转换为16进制(小写),即为签名信息,放入请求头(signature)中
二、签名值算法
a) 签名值算法java代码参考(请参考接入工具包(sm3-tools)):
public class HttpUtils {private static final RestTemplate template = new RestTemplate();
public static void main(String[] args) {
Person person = new Person("1231414234","2");
postJson("{{基础地址}}/open/signature/sealQuerysealQue", JSONObject.toJSONString(person), new HashMap<>(), "scca分配给你的app_id", "scca分配给你的app_secret")
}
public static String postJson(String uri, String json, Map<String, String> headerParams, String appId, String secret) {
HttpHeaders header = getHeaders(headerParams, json, appId, secret);
header.add("Content-Type", "application/json;charset=UTF-8");
HttpEntity<String> entity = new HttpEntity<>(json, header);
return execute(uri, entity);
}
private static HttpHeaders getHeaders(Map<String, String> headerParams, String toSign, String appId, String secret) {
HttpHeaders headers = new HttpHeaders();
if (headerParams != null) {
headerParams.forEach(headers::add);
}
String nonce = nonce();
String ts = String.valueOf(System.currentTimeMillis());
String ori = toSign.concat(nonce).concat(ts);
System.out.println(ori);
System.out.println(secret);
String signature = signature(ori, secret);
headers.add("signature", signature);
headers.add("app_id", appId);
headers.add("nonce", nonce);
headers.add("timestamp", ts);
return headers;
}
private static String nonce() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replaceAll("[^0-9]", "");
}
private static String execute(String url, HttpEntity<?> entity) throws RuntimeException {
ResponseEntity<String> result = template.postForEntity(url, entity, String.class, new Object[0]);
if (result.getStatusCodeValue() == 200) {
return (String) result.getBody();
} else {
throw new RuntimeException("请求服务器出错");
}
}
private static String signature(String toSign, String secret) {
return calc(toSign, secret);
}
private static String calc(String toSign, String secret) {
try {
byte[] bytes = HMACUtils.hmacSM3(toSign.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8));
return HMACUtils.byteArrayToHexString(bytes);
} catch (Throwable var4) {
throw new RuntimeException("hmac签名失败.", var4);
}
}
}
public class HMACUtils {
public HMACUtils() {
}
public static byte[] hmacSM3(byte[] data, byte[] sm4Key) {
return hmac(data, sm4Key);
}
public static byte[] hmac(byte[] data, byte[] key) {
KeyParameter keyParameter = new KeyParameter(key);
SM3Digest sm3Digest = new SM3Digest();
HMac hMac = new HMac(sm3Digest);
hMac.init(keyParameter);
hMac.update(data, 0, data.length);
byte[] result = new byte[hMac.getMacSize()];
hMac.doFinal(result, 0);
return result;
}
private static final char[] hex = "0123456789abcdef".toCharArray();
public static String byteArrayToHexString(byte[] bytes) {
if (null == bytes) {
return null;
}
StringBuilder sb = new StringBuilder(bytes.length << 1);
for (int i = 0; i < bytes.length; ++i) {
sb.append(hex[(bytes[i] & 0xf0) >> 4])
.append(hex[(bytes[i] & 0x0f)]) ;
}
return sb.toString();
}
}
b) 签名值算法Apifox脚本如下:
// 变量名称定义(Header取值时会被转换成小写)
var secretVariableKey = "appSecret" // secret变量名称
var signatureVariableKey = "_signature" //计算出的签名变量名称
// 这个参数只做打印, 不参与计算
var appIdVariableKey = "appId" // appID变量名称/Header名称
// 随机数 & 时间戳处理
// 签名值计算使用的时间戳变量名称/Header名称, 不需要时设置为空串
var timestampVariableKey = "timestamp"
// 签名值计算使用的随机数变量名称/Header名称, 不需要时设置为空串
var nonceVariableKey = "nonce"
/*
内置变量替换在pre script之后执行, 这里要计算签名值, 需要自定义变量才能使用(变量以`_`打头表示生成~~~)
所有变量参考:
https://learning.postman.com/docs/writing-scripts/script-references/variables-list/
*/
pm.globals.set("_timestamp", pm.variables.replaceIn('{{$timestamp}}')) // 时间戳, 秒
pm.globals.set("_timestampMs", pm.variables.replaceIn('{{$timestamp}}') * 1000) // 时间戳, MS
pm.globals.set("_randomUUID", pm.variables.replaceIn('{{$randomUUID}}'))
pm.globals.set("_randomInt", pm.variables.replaceIn('{{$randomInt}}')) // 1000以内整数
pm.globals.set("_randomCreditCardMask", pm.variables.replaceIn('{{$randomCreditCardMask}}')) // 固定4位整数
pm.globals.set("_randomBankAccount", pm.variables.replaceIn('{{$randomBankAccount}}')) // 8位随机数
pm.globals.set("_nonce", pm.variables.replaceIn('{{$randomBankAccount}}')) // 8位随机数
pm.globals.set("_randomUserName", pm.variables.replaceIn('{{$randomUserName}}'))
pm.globals.set("_randomPassword", pm.variables.replaceIn('{{$randomPassword}}'))
pm.globals.set("_randomEmail", pm.variables.replaceIn('{{$randomEmail}}'))
pm.globals.set("_randomFirstName", pm.variables.replaceIn('{{$randomFirstName}}'))
pm.globals.set("_randomLastName", pm.variables.replaceIn('{{$randomLastName}}'))
pm.globals.set("_randomCompanyName", pm.variables.replaceIn('{{$randomCompanyName}}'))
// 不常用的
pm.globals.set("_randomFilePath", pm.variables.replaceIn('{{$randomFilePath}}')) // 文件路径
pm.globals.set("_randomMimeType", pm.variables.replaceIn('{{$randomMimeType}}')) //
pm.globals.set("_randomProduct", pm.variables.replaceIn('{{$randomProduct}}')) // 随机的产品
pm.globals.set("_randomDepartment", pm.variables.replaceIn('{{$randomDepartment}}')) //商业分类: Movies, Electronics
pm.globals.set("_randomLocale", pm.variables.replaceIn('{{$randomLocale}}')) // 地点, 两位简写
pm.globals.set("_randomTransactionType", pm.variables.replaceIn('{{$randomTransactionType}}')) // 交易类型
pm.globals.set("_randomNoun", pm.variables.replaceIn('{{$randomNoun}}')) // 随机的名词: bus, bandwidth
pm.globals.set("_randomLoremParagraphs", pm.variables.replaceIn('{{$randomLoremParagraphs}}')) // 文章段落
pm.globals.set("_randomUrl", pm.variables.replaceIn('{{$randomUrl}}')) // 随机网址
pm.globals.set("_randomAlphaNumeric", pm.variables.replaceIn('{{$randomAlphaNumeric}}')) // 随机字符
pm.globals.set("_randomColor", pm.variables.replaceIn('{{$randomColor}}')) //颜色: red, blue
pm.globals.set("_randomHexColor", pm.variables.replaceIn('{{$randomHexColor}}')) // 随机Hex颜色: #47594a
pm.globals.set("_randomWeekday", pm.variables.replaceIn('{{$randomWeekday}}')) // 星期(英文)
// ============================ 开始处理签名计算
// ============================ 开始处理签名计算
// ============================ 开始处理签名计算
var keys = Object.keys(request.data), i, len = keys.length;
var requestBody = "";
// 从变量中提取值的工具函数
var replaceBodyVar = function(val){
var holder = val.match(/\{\{(\w+?)}}/g)
if(holder && holder.length){
for(var i = 0; i < holder.length; i ++){
var k = holder[i];
// 需要使用在上面代码中定义了的变量才能正常工作
var v = pm.globals.get(k.replace(/({|})/g, ''))
val = val.replace(k, v)
console.log("替换变量值: >>>> "+k+" -> "+ v)
}
console.log("替换处理后的请求参数:", requestBody)
}
return val;
}
// 替换header中的变量名称, 可能是header.key或者是环境变量中的key
var replaceHeaderVar = function(variableKey) {
var key;
var val = request.headers[variableKey]
if(/\{\{.*?\}\}/g.test(val)){
// 如果是header.key, 那么值应该是变量, 提取表达式
key = val.replace(/({|})/g, '')
} else{
// 如果为env.key, 直接获取
key = variableKey;
}
var v = pm.environment.get(key) || pm.globals.get(key)
console.log("replace header var, key = " + val + "; val = " + v)
return v
}
// 处理Secret
var secret = pm.environment.get(secretVariableKey) || pm.globals.get(secretVariableKey)
if(!secret){
throw new Error("环境变量中未配置Secret信息, secret Key : " + secretVariableKey)
}
// 处理APPID
// console.log(request.headers)
var appId = replaceHeaderVar(appIdVariableKey)
// if(!appId){
// throw new Error("Header和env中都找不到AppID信息, AppID Key : " + appIdVariableKey)
// }
// 开始计算签名值
console.log(request)
var contentType = request.headers['content-type'] || "application/x-www-form-urlencoded"
if(contentType === 'application/json'){
//ch请求json 去空格换行,保证参数计算和服务端拿到的一致
dataBody = request.data.replace(/[ ]|[\r\n]/g,"")
pm.request.body.update(dataBody)
requestBody = replaceBodyVar(request.data)
} else{
keys.sort(); //根据key经行排序
for (var index in keys) {
var param = replaceBodyVar(request.data[keys[index]])
// formdata hack: 参数为文件时, 值为空字符串
var notEmpty = param && param.length > 0;
requestBody += notEmpty ? param : '';
}
//requestBody=requestBody.substr(0, requestBody.length-1)
}
if(nonceVariableKey){
requestBody = requestBody + replaceHeaderVar(nonceVariableKey)
}
if(timestampVariableKey){
requestBody = requestBody + replaceHeaderVar(timestampVariableKey)
}
console.log("cal sig("+contentType+") using app.secret: [" + secret + "] for AppId: [" + appId + "]");
//ch计算签名参数 去空格换行,保证参数计算和服务端拿到的一致
requestBody = requestBody.replace(/[ ]|[\r\n]/g,"")
console.log("request body(签名原文22222):"+requestBody);
var signHmacSM3
fox.liveRequire("sm-crypto", (smObj) => {
try {
let secretBytes = stringToByte(secret)
signHmacSM3 = smObj.sm3(requestBody,{key:secretBytes})
console.log("sm3=",signHmacSM3)
pm.globals.set(signatureVariableKey, signHmacSM3);
} catch (error) {
console.error("An error occurred during liveRequire callback", error);
throw error;
}
});
//ch字符串转字节
function stringToByte (str) {
var len, c;
len = str.length;
var bytes = [];
for (var i = 0; i < len; i++) {
c = str.charCodeAt(i);
if (c >= 0x010000 && c <= 0x10FFFF) {
bytes.push(((c >> 18) & 0x07) | 0xF0);
bytes.push(((c >> 12) & 0x3F) | 0x80);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if (c >= 0x000800 && c <= 0x00FFFF) {
bytes.push(((c >> 12) & 0x0F) | 0xE0);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if (c >= 0x000080 && c <= 0x0007FF) {
bytes.push(((c >> 6) & 0x1F) | 0xC0);
bytes.push((c & 0x3F) | 0x80);
} else {
bytes.push(c & 0xFF);
}
}
return bytes;
}
CA机构及证照机构接入
- 应用接入指南:
《电子证照及CA机构身份认证服务接入指南》 - 认证机构接入标准:
《团体标准-电子证照及CA机构身份认证服务接入接口标准》
开源区块链平台FISCO-BECOS