Skip to content

商户签名验证拦截器 🔐

功能介绍 💡

WARNING

商户签名验证拦截器(也称签名/验签过滤器)是一个强大的安全防护工具,就像银行的防伪系统一样,通过对请求进行签名验证来确保数据的安全性和完整性。

支持的请求格式

  • application/json - JSON数据
  • application/x-www-form-urlencoded - 表单数据​
  • url地址 - URL参数传递
  • multipart/form-data - 不支持附件表单数据提交

应用场景

  • 🛡️ 防止数据篡改 - 确保数据在传输过程中未被修改
  • 🔄 防重复提交 - 避免同一请求被重复处理
  • 🕒 时效性验证 - 防止历史请求重放攻击

详细配置说明 ⚙️

核心参数说明

参数名类型必填默认值说明
enabledbooleanfalse是否启用拦截器
rulesarray[object]-规则列表

商户配置详解 (rules)

参数名类型必填默认值说明
urlsarray[string]-需要进行签名验证的URL地址列表
time-verifybooleanfalse是否启用时间验证,防止重放攻击
time-intervalDuration10m请求时间有效期,超过则视为过期
one-verifybooleanfalse是否开启一次性请求验证(需配置Redis)
one-time-intervalDuration1h一次性请求的验证时间窗口
encryptstringSHA1签名加密算法,支持多种加密方式
sign-typestringsimple签名类型:simple(仅普通参数)或all(所有参数)
parameter-typestring-参数获取位置:header 请求头,body body参数,query 查询参数
appsarray[object]-应用列表(配置多个,用于多个应用同时调用)

商户应用列表 (apps)

参数名类型必填默认值说明
app-idstring-应用标识,用于区分不同的接入方
app-secretstring-应用密钥,建议使用32位UUID,用于签名验证

加密算法支持列表

支持的加密算法包括:

  • 🔒 基础算法:MD5, SHA1, SHA256, SHA384, SHA512, SM3
  • 🔐 SHA3系列:SHA3_224, SHA3_256, SHA3_384, SHA3_512
  • 🔑 SHA512系列:SHA512_224, SHA512_256
  • 🛡️ HMAC系列:HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, HMAC_SHA512, HMAC_SM3

配置示例 📝

基础配置示例

yaml
# 基础商户签名验证配置
gateway:
  filter:
    merchant:
      enabled: true  # 启用商户签名验证
      rules:
        - urls:  # 需要验证的URL
            - /demo/login
          time-verify: true  # 启用时间验证
          time-interval: 30m  # 30分钟有效期
          one-verify: true  # 启用一次性验证
          one-time-interval: 1h  # 1小时验证窗口
          sign-type: all  # 全参数签名
          apps:
            - app-secret: 11e1ebfd58254b84a6f3c1d81d27a562
              app-id: test

完整配置示例

yaml
# 完整的商户签名验证配置示例
gateway:
  filter:
    merchant:
      enabled: true
      rules:
        # API接口签名验证
        - urls:
            - /api/payment/**
            - /api/order/**
          time-verify: true  # 启用时间验证
          time-interval: 10m  # 10分钟有效期
          one-verify: true  # 启用一次性验证
          one-time-interval: 1h  # 1小时验证窗口
          encrypt: SHA256  # 使用SHA256加密
          sign-type: all  # 全参数签名
          parameter-type: header  # 签名参数通过请求头传递
          apps:
            - app-id: merchant-001
              app-secret: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
            - app-id: merchant-002
              app-secret: z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4
        
        # 开放API接口(简单签名)
        - urls:
            - /open/api/**
          time-verify: false  # 不启用时间验证
          one-verify: false  # 不启用一次性验证
          encrypt: SHA1  # 使用SHA1加密
          sign-type: simple  # 简单参数签名
          parameter-type: body  # 签名参数通过body传递
          apps:
            - app-id: open-api-client
              app-secret: openapi123456789abcdef0123456789abcdef

高安全级别配置

yaml
# 高安全级别的商户签名验证配置
gateway:
  filter:
    merchant:
      enabled: true
      rules:
        - urls:
            - /secure/api/**
            - /financial/**
          time-verify: true
          time-interval: 5m  # 5分钟有效期(更严格)
          one-verify: true
          one-time-interval: 30m  # 30分钟验证窗口
          encrypt: HMAC_SHA256  # 使用HMAC-SHA256加密
          sign-type: all
          parameter-type: header
          apps:
            - app-id: secure-client-001
              app-secret: 1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t

签名参数详解 📋

签名参数传输方式

推荐使用header方式传输签名参数(配置parameter-type=header),使用以下请求头名称:

请求头名称必选说明示例值
X-Merchant-Id应用标识test
X-Merchant-Random-Str随机字符串ce9d5159-1c79-4572-b4b1-4640ef03e46b
X-Merchant-Timestamp时间戳(毫秒)1617696265321
X-Merchant-Sign签名结果e06e59858fb81835030df94db42305d8780acc33

也可以使用body或query方式传输签名参数,但不推荐:

参数名必选类型说明示例值
appIdstring应用标识,与配置中的app-id对应test
randomStrstring随机字符串,用于防重放攻击ce9d5159-1c79-4572-b4b1-4640ef03e46b
timestampstring请求时间戳(毫秒)1617696265321
signstring签名结果e06e59858fb81835030df94db42305d8780acc33

签名规则说明 📜

  1. 参数排序:所有参数按照参数名ASCII码升序排序
  2. 参数拼接:格式为 参数名=参数值&参数名=参数值
  3. JSON处理
    • 仅支持对象格式 {}
    • 不支持数组或字符串作为根节点
    • simple模式:只签名普通类型字段
    • all模式:签名所有层级字段
  4. 默认签名参数
    • appId: 应用标识
    • appSecret: 应用密钥(不能在请求中传输)
    • timestamp: 时间戳(毫秒)
    • randomStr: 随机字符串

TIP

  • 可以选择通过请求头或请求参数传递签名信息
  • 建议使用请求头方式,可以避免签名参数污染URL
  • 请求头名称必须严格匹配,区分大小写

重要安全提示 ⚠️

CAUTION

  1. appSecret 是签名的核心密钥,严禁在接口参数中传输
  2. 建议在客户端进行代码混淆,提高安全性
  3. 定期更换 appSecret,降低密钥泄露风险
  4. 生产环境建议使用更安全的加密算法(如 SHA256)

示例

java签名示例

java
package com.wueasy;

import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;

import org.apache.commons.codec.digest.DigestUtils;

import com.fasterxml.jackson.databind.JsonNode;
import com.wueasy.base.util.JsonHelper;
import com.wueasy.base.util.StringHelper;

public class Test {

	public static void main(String[] args) {

		String bodyString = "{\"token\":\"7a081f88ef3b62922c8777d143e90cf0\",\"user\":{\"userId\":\"123\",\"email\":\"123@qq.com\"},\"authorizeCodeList\":[\"1\",\"2\",\"3\"],\"menuList\":[{\"menuId\":\"1\",\"menuName\":\"test\"},{\"menuId\":\"1\",\"menuName\":\"test\"}],\"extendedObject\":{\"a\":\"132******34\",\"user2\":{\"userId\":\"123\",\"email\":\"123@qq.com\"},\"list\":[{\"menuId\":\"1\",\"menuName\":\"test\"},{\"menuId\":\"1\",\"menuName\":\"test\"}]},\"createTime\":1654610049815}";
		
		String appId = "test"; //应用id
		String appSecret = "11e1ebfd58254b84a6f3c1d81d27a562";//应用密钥,需要保密,不能传入参数

		String randomStr = UUID.randomUUID().toString();//随机字符串

		Long timestamp =System.currentTimeMillis();//时间戳,毫秒

		//签名参数,按照参数名称升序排序
		Map<String, String> signMap = getSignMap(bodyString, "ALL");

		signMap.put("appId", appId);
		signMap.put("appSecret", appSecret);
		signMap.put("timestamp", timestamp+"");
		signMap.put("randomStr", randomStr);

		//拼接签名字符串
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, String> entry : signMap.entrySet()) {
			String value = entry.getValue();
			sb.append(entry.getKey()).append("=").append(value).append("&");
		}
		String signStr = sb.deleteCharAt(sb.length() - 1).toString();

		System.err.println(signStr);
		
		//获取签名后的内容值,使用sha1加密
		String sign = DigestUtils.sha1Hex(signStr);
		System.err.println(sign);

		

		System.err.println(JsonHelper.toJsonString(signMap));
	}

	/**
	 * 获取参数签名map
	 * @author: fallsea
	 * @param bodyString
	 * @param signType
	 * @return
	 */
	public static Map<String, String> getSignMap(String bodyString, String signType) {
		Map<String, String> signMap = new TreeMap<>(new Comparator<String>() {
			public int compare(String obj1, String obj2) {
				return obj1.compareTo(obj2);
			}
		});

		if (StringHelper.isNotBlank(bodyString)) {
			if (signType == "ALL") {
				JsonNode json = JsonHelper.parseTree(bodyString);
				getSignAllMap("", json, signMap);
			} else {
				JsonNode json = JsonHelper.parseTree(bodyString);
				json.fieldNames().forEachRemaining((fieldName) -> {
					JsonNode item = json.get(fieldName);
					if (!item.isArray() && !item.isObject() && !item.isNull()) {
						signMap.put(fieldName, item.asText());
					}
				});
			}
		}
		return signMap;
	}

	private static void getSignAllMap(String path,JsonNode json,Map<String, String> signMap) {
		if(null!=json && !json.isNull()) {
			if(json.isObject()) {
				json.fieldNames().forEachRemaining((fieldName)->{
					JsonNode item = json.get(fieldName);
					String path2 = fieldName;
					if(StringHelper.isNotBlank(path)) {
						path2 = path+"."+fieldName;
					}
					if(!item.isNull()) {
						if(item.isObject()) {
							getSignAllMap(path2,item, signMap);
						}else if(item.isArray()) {
							for (int i = 0; i < item.size(); i++) {
								getSignAllMap(path2+"["+i+"]",item.get(i), signMap);
							}
						}else {
							signMap.put(path2, item.asText());
						}
					}else {
						signMap.put(path2, "");
					}
				});
			}else if(json.isArray()){
				for (int i = 0; i < json.size(); i++) {
					getSignAllMap(path+"["+i+"]",json.get(i), signMap);
				}
			}else {
				signMap.put(path, json.asText());
			}
		}else {
			signMap.put(path, "");
		}
	}

}

js签名示例

javascript
var bodyString = "{\"token\":\"7a081f88ef3b62922c8777d143e90cf0\",\"user\":{\"userId\":\"123\",\"email\":\"123@qq.com\"},\"authorizeCodeList\":[\"1\",\"2\",\"3\"],\"menuList\":[{\"menuId\":\"1\",\"menuName\":\"test\"},{\"menuId\":\"1\",\"menuName\":\"test\"}],\"extendedObject\":{\"a\":\"132******34\",\"user2\":{\"userId\":\"123\",\"email\":\"123@qq.com\"},\"list\":[{\"menuId\":\"1\",\"menuName\":\"test\"},{\"menuId\":\"1\",\"menuName\":\"test\"}]},\"createTime\":1654610049815}";

console.log(JSON.parse(bodyString))

var obj = JSON.parse(bodyString);


function getSignAllMap(path,json,signJson){
	if(null!=json){
		let type = Object.prototype.toString.call(json);
		if(type == "[object Object]"){
			for(let key in json){
				let value = json[key];
				let path2 = key;
				if (path) {
					path2 = path + "." + key;
				}
				if(null!=value && ""!=value && undefined != value){
					let type2 = Object.prototype.toString.call(value);
					
					if(type2 == "[object Object]"){
						getSignAllMap(path2,value,signJson)
					}else if(type2 == "[object Array]"){
						for(let i=0;i<value.length;i++){
							getSignAllMap(path2+"["+i+"]",value[i],signJson)
						}
					}else {
						signJson[path2] = value;
					}
				}else{
					signJson[path2] = "";
				}
			}
		}else if(type == "[object Array]"){
			for(let i=0;i<json.length;i++){
				getSignAllMap(path+"["+i+"]",json[i],signJson)
			}
		}else{
			signJson[path] = json;
		}
	}else{
		signJson[path] = "";
	}
}





function getSignStr(json){
	let signJson = {};
	getSignAllMap("",json,signJson)
	
	// 取 key
	let keys = [];
	for (let key in signJson) {
	   keys.push(key);
	}

	// 参数名 ASCII 码从小到大排序(字典序)
	keys.sort();
	
	let signStr = "";
	
	for (let i=0;i<keys.length;i++) {
		if(""!=signStr){
			signStr += "&"
		}
		signStr += keys[i] + "=" + signJson[keys[i]]
	}
	
	return signStr;

}

let signStr= getSignStr(obj)

console.log(signStr)

常见问题 ❓

  1. 签名验证失败

    • 检查参数排序是否正确
    • 验证时间戳是否在有效期内
    • 确认签名算法是否匹配
  2. 重复请求被拦截

    • 检查 randomStr 是否重复使用
    • 确认时间戳是否在有效期内
  3. 配置未生效

    • 验证 enabled 是否为 true
    • 检查 URL 是否在配置列表中
    • 确认请求方式是否支持