Skip to content

商户签名验证拦截器 🔐

功能介绍 💡

WARNING

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

支持的请求格式

  • application/x-www-form-urlencoded - 表单数据
  • application/json - JSON数据
  • url地址 - URL参数传递

应用场景

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

详细配置说明 ⚙️

核心参数说明

参数名类型必填默认值说明
enabledbooleanfalse是否启用拦截器
itemsarray-商户配置列表

商户配置详解 (items)

参数名类型必填默认值说明
app-idstring-应用标识,用于区分不同的接入方
app-secretstring-应用密钥,建议使用32位UUID,用于签名验证
namestring-商户名称,便于管理和识别
time-verifybooleanfalse是否启用时间验证,防止重放攻击
time-intervalstringPT30M请求时间有效期,超过则视为过期
one-verifybooleanfalse是否开启一次性请求验证(需配置Redis)
one-time-intervalstringPT1H一次性请求的验证时间窗口
encryptstringSHA1签名加密算法,支持多种加密方式
sign-typestringSIMPLE签名类型:SIMPLE(仅普通参数)或ALL(所有参数)
urlsarray-需要进行签名验证的URL地址列表
parameter-typestringquery参数获取位置:HEADER 请求头,BODY body参数,QUERY 查询参数
headersobject-请求头过滤规则(v1.5.2+)

加密算法支持列表

支持的加密算法包括:

  • 🔒 基础算法:MD2, MD5, SHA1, SHA256, SHA384, SHA512
  • 🔐 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

配置示例 📝

yaml
wueasy:
  gateway:
    filter:
      merchant:
        enabled: true  # 启用商户签名验证
        items:
        - app-id: test  # 应用标识
          app-secret: 11e1ebfd58254b84a6f3c1d81d27a562  # 应用密钥
          name: 测试商户  # 商户名称
          time-verify: true  # 启用时间验证
          time-interval: PT30M  # 30分钟有效期
          one-verify: true  # 启用一次性验证
          one-time-interval: PT1H  # 1小时验证窗口
          sign-type: ALL  # 全参数签名
          urls:  # 需要验证的URL
          - /demo/login
          headers:  # 请求头过滤(可选)
            appId:  
            - test
            - demo

签名参数详解 📋

必传参数说明

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

签名规则说明 📜

  1. 参数排序:所有参数按照参数名ASCII码升序排序
  2. 参数拼接:格式为 参数名=参数值&参数名=参数值
  3. JSON处理
    • 仅支持对象格式 {}
    • 不支持数组或字符串作为根节点
    • SIMPLE模式:只签名普通类型字段
    • ALL模式:签名所有层级字段

重要安全提示 ⚠️

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