Skip to content

商户签名验证拦截器 🔐

功能介绍 💡

WARNING

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

支持的请求格式

  • application/json - JSON数据
  • url地址 - URL参数传递

应用场景

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

详细配置说明 ⚙️

核心参数说明

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

商户配置详解 (rules)

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

商户应用列表 (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

签名参数详解 📋

签名参数传输方式

推荐使用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 是否在配置列表中
    • 确认请求方式是否支持