Skip to content

商户签名验证拦截器 🔐

功能介绍 💡

WARNING

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

🎯 核心功能

  • 数据完整性验证 - 确保请求数据未被篡改
  • 身份认证 - 验证请求方的合法身份
  • 防重放攻击 - 防止历史请求被恶意重复使用
  • 时效性控制 - 限制请求的有效时间窗口
  • 路径绑定签名 - 防止签名在不同接口间复用

📝 支持的请求格式

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

🛡️ 应用场景

  • API接口安全 - 保护敏感API接口免受恶意调用
  • 支付系统 - 确保支付请求的安全性和唯一性
  • 数据同步 - 保证系统间数据传输的完整性
  • 第三方集成 - 验证合作伙伴的API调用合法性

详细配置说明 ⚙️

核心参数说明

参数名类型必填默认值说明
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 查询参数
path-includebooleanfalse是否将请求路径包含在签名中,启用后可防止签名在不同接口间复用
predicatesarray[string]-断言条件列表,用于更精确地匹配请求,支持Method、Header、Host、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

断言配置

断言说明

断言用于判断请求是否匹配当前路由,支持多种匹配方式。合理配置断言可以实现精确的路由控制和灵活的请求分发策略。

📋 断言类型速查表

断言类型核心功能配置格式典型示例使用场景
Method 🔧HTTP方法匹配Method=方法1,方法2,...Method=GET,POST,PUTRESTful接口、操作权限控制
Header 📋请求头匹配(支持正则)Header=名称,正则表达式Header=X-Id,\d+认证鉴权、版本控制
Query查询参数匹配Query=参数名,正则表达式Query=name,test参数校验、条件过滤
Host 🌐主机名匹配(支持正则)Host=主机名1,主机名2,...Host=demo\.wueasy\.com多域名部署、环境隔离

🎯 断言配置最佳实践

🔐 1. 请求方法控制(Method)

yaml
# RESTful接口设计
Method=GET                # 查询操作
Method=POST               # 创建操作
Method=PUT,PATCH          # 更新操作
Method=DELETE             # 删除操作

# 权限控制组合
Method=GET                # 只读接口
Method=POST,PUT,DELETE    # 写操作接口

📊 2. 请求头匹配(Header)- 认证利器

yaml
# API版本控制
Header=X-API-Version,v1\..*    # v1.x版本
Header=X-API-Version,v2\.\d+   # v2.数字版本

# 认证令牌验证
Header=Authorization,Bearer\s.+ # Bearer Token格式
Header=X-API-Key,\w{32}         # 32位API密钥

# 客户端类型识别
Header=User-Agent,.*Mobile.*    # 移动端请求
Header=Content-Type,application/json # JSON格式请求

🔍 3. 查询参数匹配(Query)

yaml
# 参数存在性检查
Query=debug                    # 只要存在debug参数就匹配
Query=debug,true               # debug参数值必须为true
Query=version,v\d+\.\d+        # version参数格式验证

# 多参数组合
Query=page,\d+                # 分页参数必须是数字
Query=sort,(asc|desc)         # 排序参数只能是asc或desc

🌐 4. 主机名匹配(Host)- 多环境部署

yaml
# 生产环境
Host=api\.wueasy\.com          # 精确匹配

# 测试环境(支持子域名)
Host=.*\.test\.wueasy\.com      # 匹配所有测试子域名
Host=^([a-z]+)\.test\.wueasy\.com$ # 命名捕获组匹配

# 多域名支持
Host=api\.wueasy\.com,api\.wueasy\.cn # 国内外域名

配置示例 📝

基础配置示例

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  # 全参数签名
          path-include: true  # 是否将请求路径包含在签名中
          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  # 签名参数通过请求头传递
          path-include: true  # 是否将请求路径包含在签名中
          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传递
          path-include: false  # 不将请求路径包含在签名中
          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

断言配置示例

yaml
# 使用断言条件的商户签名验证配置
gateway:
  filter:
    merchant:
      enabled: true
      rules:
        # 仅对POST请求进行签名验证
        - urls:
            - /api/user/**
          predicates:
            - Method=POST,PUT,DELETE  # 只对修改操作进行签名验证
          time-verify: true
          time-interval: 10m
          encrypt: SHA256
          sign-type: all
          apps:
            - app-id: user-api-client
              app-secret: user123456789abcdef0123456789abcdef
        
        # 根据请求头区分不同的签名配置
        - urls:
            - /api/v2/**
          predicates:
            - Header=X-API-Version,v2  # 只对API v2版本进行签名验证
            - Header=Content-Type,application/json  # 只对JSON请求进行验证
          time-verify: true
          time-interval: 15m
          encrypt: SHA256
          sign-type: all
          path-include: true
          apps:
            - app-id: v2-api-client
              app-secret: v2api123456789abcdef0123456789abcdef
        
        # 根据主机名和查询参数进行断言
        - urls:
            - /api/payment/**
          predicates:
            - Host=api\.payment\.example\.com  # 只匹配支付API域名
            - Query=version,v3  # 只匹配v3版本的支付API
          time-verify: true
          time-interval: 5m  # 支付API使用更短的有效期
          one-verify: true
          encrypt: HMAC_SHA256
          sign-type: all
          parameter-type: header
          path-include: true
          apps:
            - app-id: payment-client
              app-secret: payment123456789abcdef0123456789abcdef
        
        # 调试模式的特殊配置
        - urls:
            - /debug/**
          predicates:
            - Query=debug  # 只有包含debug参数的请求才进行签名验证
            - Method=GET,POST  # 只对GET和POST请求进行验证
          time-verify: false  # 调试模式不启用时间验证
          one-verify: false
          encrypt: SHA1  # 使用较简单的加密算法
          sign-type: simple
          apps:
            - app-id: debug-client
              app-secret: debug123456789abcdef0123456789abcdef

签名参数详解 📋

签名参数传输方式

推荐使用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: 随机字符串
  5. 路径包含签名(可选):
    • 当配置 path-include: true 时,会将请求路径作为 path 参数加入签名
    • 路径参数值为完整的请求路径,如:/api/payment/create
    • 启用路径签名可以防止同一签名被用于不同的API接口
    • 提高安全性,防止签名重放攻击跨接口使用

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);
		
		// 如果启用了路径包含签名(path-include: true),需要添加path参数
		// signMap.put("path", "/api/payment/create"); // 请求路径

		//拼接签名字符串
		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, "");
		}
	}

}

🌐 JavaScript签名示例

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. 签名验证失败

    • 检查参数排序是否正确
    • 验证时间戳是否在有效期内
    • 确认签名算法是否匹配
    • 检查是否正确包含了路径参数(当启用 path-include 时)
  2. 重复请求被拦截

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

    • 验证 enabled 是否为 true
    • 检查 URL 是否在配置列表中
    • 确认请求方式是否支持
  4. 路径包含签名问题

    • 确认配置中 path-include 是否正确设置
    • 检查签名计算时是否包含了正确的请求路径
    • 验证路径参数值是否与实际请求路径一致
  5. 断言配置问题

    • 检查断言条件格式是否正确(如 Method=GET,POST
    • 验证所有断言条件是否都满足(多个断言是"与"关系)
    • 确认断言中的模式匹配是否正确(支持Ant风格通配符)
    • 检查请求头、查询参数等是否与断言条件匹配
  6. 断言不生效

    • 确认断言条件语法是否正确
    • 检查请求是否满足所有配置的断言条件
    • 验证断言条件中的值是否区分大小写
    • 确认Host断言是否与实际请求的Host头匹配