商户过滤器

商户过滤器又称签名/验签过滤器,对部分请求需要通过签名方式来验证请求安全性。

拦截地址配置

配置后,对需要验证的地址进行验证拦截

  • enabled:是否启用拦截器,默认不启用false
  • urls:需要拦截的地址

url匹配规则:

  • 模糊匹配:可以通过/**/*模糊匹配所有地址和路径下地址
wueasy:
  gateway:
    interceptor:
      merchant:
        enabled: true #是否启用,默认false
        urls: #需要拦截的地址
        - /**

商户配置

需要配置商户的信息,merchants中可以配置多个。

  • appId:应用id
  • appSecret:应用密钥(建议32位随机字符串,可以使用uuid
  • name:商户的名称
  • timeVerify:是否启用时间验证,默认false,例如:现在重复请求一小时之前的数据,提示请求过期
  • timeInterval:时间验证区间,秒,默认30分钟,启用时间验证后有效
  • oneVerify:是否请求一次有效,默认false,解决一次消息重复多次的问题,需要配置redis
  • oneTimeInterval:一次请求的验证时间,秒,默认1小时
  • encrypt:签名加密方式,默认SHA1,可选值 MD2,MD5,SHA1,SHA256,SHA384,SHA512,SHA3_224,SHA3_256,SHA3_384,SHA3_512,SHA512_224,SHA512_256,HMAC_MD5,HMAC_SHA_1,HMAC_SHA_224,HMAC_SHA_256,HMAC_SHA_384,HMAC_SHA_512
  • signType:签名类型,默认SIMPLESIMPLE:简单的(只需要对普通的类型参数进行签名);ALL:需要对全部参数进行签名。5.0.0+新增
wueasy:
  gateway:
    merchants:
    - appId: test #应用id
      appSecret: 11e1ebfd58254b84a6f3c1d81d27a562 #应用密钥
      name: 测试 #商户名称
      timeVerify: false #是否启用时间验证,默认false
      timeInterval: 1800 #时间验证区间,秒,默认30分钟
      oneVerify: false #是否请求一次有效,默认false
      oneTimeInterval: 3600 #一次请求的验证时间,秒,默认1小时

redis配置

由于一次请求有效是通过reids进行存储验证的,所以如果启用oneVerify,就需要配置对应的商户redis,配置必须为merchant

wueasy :
  data:
    redis: #redis配置
      merchant: #商户配置
        database : 1 #数据库索引(默认为0)
        host : 127.0.0.1 #服务器地址
        port : 6379 #服务器连接端口
        password : 123456  #服务器连接密码(默认为空)
        pool : #连接池配置
          maxIdle : 8 # 连接池中的最大空闲连接,默认值也是8。
          minIdle : 0 #连接池中的最小空闲连接,默认值也是0。
          maxTotal : 2000 # 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
          maxWaitMillis : 1000 # 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException
        timeout : 3000 #连接超时时间(毫秒)

客户端签名demo

  1. 签名参数必须拼接在url地址中,例如:http://127.0.0.1:8080/demo/login?appId=test&randomStr=ce9d5159-1c79-4572-b4b1-4640ef03e46b&timestamp=1617696265321&sign=e06e59858fb81835030df94db42305d8780acc33
  2. 需要对所有的参数都进行拼接,最后得到签名值,按照参数名称升序排序
  3. 如果提交数据的方式为application/json,那么body必须是json对象{},不允许是其他的类型(数组或字符串,对象中可以包含任意类型),并且签名时,需要排除数据和对象字段,只需要普通类型字段

需要传入的参数

参数名 必选 类型 说明
appId string 应用id
randomStr string 随机字符串
timestamp long 时间戳,毫秒
sign string 签名后的值

java签名demo

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);
                    if (!item.isNull()) {
                        String path2 = fieldName;
                        if (StringHelper.isNotBlank(path)) {
                            path2 = path + "." + fieldName;
                        }
                        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 if (json.isArray()) {
                for (int i = 0; i < json.size(); i++) {
                    getSignAllMap(path + "[" + i + "]", json.get(i), signMap);
                }
            } else {
                signMap.put(path, json.asText());
            }
        }
    }

}

js签名demo

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];
                if(null!=value && ""!=value && undefined != value){
                    let type2 = Object.prototype.toString.call(value);
                    let path2 = key;
                    if (path) {
                        path2 = path + "." + key;
                    }
                    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 {
                        //console.log(path2+"="+value)
                        signJson[path2] = value;
                    }
                }
            }
        }else if(type == "[object Array]"){
            for(let i=0;i<json.length;i++){
                getSignAllMap(path+"["+i+"]",json[i],signJson)
            }

        }else{
            //console.log(path+"="+json)
            signJson[path] = json;
        }

    }
}





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)

重要说明

appSecret作为加密的密钥,一定不能暴露在接口参数中。 最好客户端业务代码进行加密/解密操作,并且混淆代码,提高安全性。

Copyright © wueasy.com 2017-2022 all right reserved,powered by Gitbook未经允许,禁止以任何形式传播 修订时间: 2022-06-18

results matching ""

    No results matching ""