外观
商户签名验证拦截器 🔐
功能介绍 💡
WARNING
商户签名验证拦截器(也称签名/验签过滤器
)是一个强大的安全防护工具,就像银行的防伪系统一样,通过对请求进行签名验证来确保数据的安全性和完整性。
支持的请求格式
- ✅
application/x-www-form-urlencoded
- 表单数据 - ✅
application/json
- JSON数据 - ✅
url地址
- URL参数传递
应用场景
- 🛡️ 防止数据篡改 - 确保数据在传输过程中未被修改
- 🔄 防重复提交 - 避免同一请求被重复处理
- 🕒 时效性验证 - 防止历史请求重放攻击
详细配置说明 ⚙️
核心参数说明
参数名 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|
enabled | boolean | 否 | false | 是否启用拦截器 |
items | array | 是 | - | 商户配置列表 |
商户配置详解 (items)
参数名 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|
app-id | string | 是 | - | 应用标识,用于区分不同的接入方 |
app-secret | string | 是 | - | 应用密钥,建议使用32位UUID,用于签名验证 |
name | string | 否 | - | 商户名称,便于管理和识别 |
time-verify | boolean | 否 | false | 是否启用时间验证,防止重放攻击 |
time-interval | string | 否 | PT30M | 请求时间有效期,超过则视为过期 |
one-verify | boolean | 否 | false | 是否开启一次性请求验证(需配置Redis) |
one-time-interval | string | 否 | PT1H | 一次性请求的验证时间窗口 |
encrypt | string | 否 | SHA1 | 签名加密算法,支持多种加密方式 |
sign-type | string | 否 | SIMPLE | 签名类型:SIMPLE(仅普通参数)或ALL(所有参数) |
urls | array | 是 | - | 需要进行签名验证的URL地址列表 |
parameter-type | string | 否 | query | 参数获取位置:HEADER 请求头,BODY body参数,QUERY 查询参数 |
headers | object | 否 | - | 请求头过滤规则(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
签名参数详解 📋
必传参数说明
参数名 | 必选 | 类型 | 说明 | 示例值 |
---|---|---|---|---|
appId | 是 | string | 应用标识,与配置中的app-id对应 | test |
randomStr | 是 | string | 随机字符串,用于防重放攻击 | ce9d5159-1c79-4572-b4b1-4640ef03e46b |
timestamp | 是 | long | 请求时间戳(毫秒) | 1617696265321 |
sign | 是 | string | 签名结果 | e06e59858fb81835030df94db42305d8780acc33 |
签名规则说明 📜
- 参数排序:所有参数按照参数名ASCII码升序排序
- 参数拼接:格式为
参数名=参数值&参数名=参数值
- JSON处理:
- 仅支持对象格式
{}
- 不支持数组或字符串作为根节点
- SIMPLE模式:只签名普通类型字段
- ALL模式:签名所有层级字段
- 仅支持对象格式
重要安全提示 ⚠️
CAUTION
appSecret
是签名的核心密钥,严禁在接口参数中传输- 建议在客户端进行代码混淆,提高安全性
- 定期更换
appSecret
,降低密钥泄露风险 - 生产环境建议使用更安全的加密算法(如 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)
常见问题 ❓
签名验证失败
- 检查参数排序是否正确
- 验证时间戳是否在有效期内
- 确认签名算法是否匹配
重复请求被拦截
- 检查
randomStr
是否重复使用 - 确认时间戳是否在有效期内
- 检查
配置未生效
- 验证
enabled
是否为 true - 检查 URL 是否在配置列表中
- 确认请求方式是否支持
- 验证