为了保证回调通知数据推送的安全,e签宝提供IP白名单模式和签名验签模式两种模式供开发者选择。
两种模式可以单独使用,也可以组合使用。
1. IP白名单模式
开发者可以采用IP白名单机制来保障回调通知接收服务的安全。如果贵司需要防火墙配置后才允许e签宝消息通知服务推送数据,请根据下方信息进行贵司防火墙设置。
环境 | 公网IP |
沙箱模拟环境 | 47.96.79.204 |
正式生产环境 | 118.31.35.8 |
以下以Nginx服务器为例,介绍如何配置防火墙白名单来允许e签宝回调通知服务入站。
(1)新建白名单文件 ip_white.conf
新建文件ip_white.conf,内容如下:
allow 47.96.79.204;
allow 118.31.35.8;
(2)nginx.conf 配置示例
#geoIP的白名单设置
geo $remote_addr $ip_whitelist {
default 0;
include ip_white.conf;
}
location /console {
proxy_redirect off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
if ( $ip_whitelist = 1 ) {
proxy_pass http://127.0.0.1:8000;
break;
}
return 403;
}
(3)Java中获取请求IP代码示例
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if( ip.indexOf(",")!=-1 ){
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
2. 签名验签模式
开发者在接收到回调通知时,可以借助请求头 Header 中的信息对推送的数据进行签名验签。
请求头 Header 参数如下:
参数名称 | 参数类型 | 参数说明 |
X-Tsign-Open-App-Id | header | 客户发生业务时的项目ID |
X-Tsign-Open-SIGNATURE | header | 签名值 |
X-Tsign-Open-TIMESTAMP | header | 时间戳 |
X-Tsign-Open-SIGNATURE-ALGORITHM | header | 使用的算法, 默认算法 hmac-sha256 |
以算法hmac-sha256签名验证做说明,验签的数据有四部分
- 1、时间戳
回调header的X-Tsign-Open-TIMESTAMP
- 2、query请求的数据
开发者设置的回调地址可能包含query数据,例如callback?accountId=aaa&orderNo=001,多个 Query 参数需要对 key 按字典(ASCII码)升序排序后,再按照value1+value2方法拼接。(e签宝平台不会追加任何参数)
- 3、body 数据
即通知实际内容,按照整体的字节流来处理
- 4、应用secret
开发者在e签宝开放平台,生成应用ID时对应的应用secret
JAVA代码示例
【点击下载回调通知签名验签JAVA_Demo:XYAPINotifySafe.zip】
PHP代码示例
<?php
callback();
//签署回调
function callback(){
// 此处可以打印下日志
$file = fopen('callback.log', "a");
fwrite($file, "startTime".date('Y-m-d H:i:s'));
if($_SERVER['REQUEST_METHOD'] != 'POST'){
fwrite($file,'非法回调');exit;
}
fwrite($file, json_encode($_SERVER));
// 校验签名 如果header里放入的值为X_TSIGN_OPEN_SIGNATURE,到header里会自动加上HTTP_,并且转化为大写,取值时如下
if(!isset($_SERVER['HTTP_X_TSIGN_OPEN_SIGNATURE'])){
echo "签名不能为空";exit;
}
$sign = $_SERVER['HTTP_X_TSIGN_OPEN_SIGNATURE'];
fwrite($file,'sign:'.$sign);
$secret = 'xxxxx';//应用对应密钥
//1.获取时间戳的字节流
if(!isset($_SERVER['HTTP_X_TSIGN_OPEN_TIMESTAMP'])){
echo "时间戳不能为空";exit;
}
$timeStamp = $_SERVER['HTTP_X_TSIGN_OPEN_TIMESTAMP'];
//2.获取query请求的字节流,对 Query 参数按照字典对 Key 进行排序后,按照value1+value2方法拼接
$params = $_GET;
if(!empty($params)){
ksort($params);
}
$requestQuery = '';
foreach($params as $val){
$requestQuery .= $val;
}
fwrite($file,'获取query的数据:'.$requestQuery);
//3. 获取body的数据
$body = file_get_contents("php://input");
fwrite($file,'获取body的数据:'.$body);
//4.组装数据并计算签名
$data = $timeStamp . $requestQuery . $body;
fwrite($file,'组装数据并计算签名:'.$data);
echo $sign;
$mySign = hash_hmac("sha256", $data, $secret);
echo $mySign;
if($mySign != $sign){
echo '验签失败';
fwrite($file,"签名校验失败");
}else{
echo '验签成功';
}
$result = json_decode($body,true);
switch ($result['action']){
case 'SIGN_MISSON_COMPLETE':
//签署人签署完成回调
break;
case 'SIGN_FLOW_COMPLETE':
//流程结束逻辑处理
break;
}
}
.NET代码示例
public static void notify()
{
//异步通知获取到的header头中的签名值:X-Tsign-Open-SIGNATURE
string signture = "4009ffb1c50d3c12c977b8XXXXXXX0605aaf5dc55dce1d4fc1c1f39c958";
//异步通知获取到的header头中的时间戳:X-Tsign-Open-TIMESTAMP
string timeStamp = "1703756522169";
//案例:异步通知请求地址 "notifyUrl": "http://demo.tsign.cn/notify?orderNo=001&belong=pinjie",
//案例:"pinjie"和"001"是开发者异步地址上拼接的Query参数值,多个Query参数需要对 key 按字典(ASCII码)升序排序后,再按照value1+value2方法拼接
//如果异步通知地址没有拼接Query,那么这里不需要参与签名计算
string query = "pinjie001";
//应用secret
string secret = "xxxx4d8f922b898ac519b4cf";
//异步通知获取到的body体请求参数
string bodyData = "{\"action\":\"SIGN_MISSON_COMPLETE\",\"timestamp\":1703756522164,\"signFlowId\":\"5ed6b3******dcdeddc23ebf\",\"customBizNum\":\"自定义编码001\",\"signOrder\":1,\"operateTime\":1703756521000,\"signResult\":2,\"resultDescription\":\"签署完成\",\"organization\":{\"orgId\":\"842ec8c******91662f\",\"orgName\":\"测试有限公司\"}}";
//最终参与验签的请求参数
string data = timeStamp + query + bodyData;
//计算签名方法
string MYSIGN = GetSignature(data, secret);
string mysign = MYSIGN.ToLower();
Console.Write("mysign=" + mysign);
if (mysign.Equals(signture))
{
MessageBox.Show("验签成功");
}
else
{
MessageBox.Show("验签失败");
}
}
public static string GetSignature(string data, string secret)
{
byte[] keyByte = Encoding.UTF8.GetBytes(secret);
byte[] messageBytes = Encoding.UTF8.GetBytes(data);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
StringBuilder sb = new StringBuilder();
foreach (byte test in hashmessage)
{
sb.Append(test.ToString("X2"));
}
return sb.ToString();
}
}