gateway 网关接口防篡改验签

gateway 网关接口防篡改验签

背景:为了尽可能降低接口在传输过程中,被抓包然后篡改接口内的参数的可能,我们可以考虑对接口的所有入参做签名验证,后端在网关依照相同的算法生成签名做匹配,不能匹配的返回错误。

主要流程:

具体前端处理:

    // 手动生成随机数
    const nonce =
      Math.random().toString(36).slice(-10) +
      Math.random().toString(36).slice(-10);
    // 生成当前的时间戳
    const timestamp = dayjs().format("YYYYMMDDHHmmss");
    const query =
      config.method.toLocaleLowerCase() === "post"
        ? config.data
        : config.params;
    // 签名生成
    config.headers["signature"] = signMd5Utils.getSign(
      requestId,
      timestamp,
      { ...query},
      config.method.toLocaleLowerCase()
    );
    config.headers["nonce"] = nonce ;
    config.headers["timestamp"] = timestamp;
export default class signMd5Utils {
  /**
   * json参数升序
   * @param jsonObj 发送参数
   */
  static sortAsc(jsonObj) {
    let arr = new Array();
    let num = 0;
    for (let i in jsonObj) {
      arr[num] = i;
      num++;
    }
    let sortArr = arr.sort();
    let sortObj = {};
    for (let i in sortArr) {
      sortObj[sortArr[i]] = jsonObj[sortArr[i]];
    }
    return sortObj;
  }

  /**
   * @param url 请求的url,应该包含请求参数(url的?后面的参数)
   * @param requestParams 请求参数(POST的JSON参数)
   * @returns {string} 获取签名
   */
  static getSign(nonce, timestamp, query = {}, method) {
    // 注意get请求入参不能太复杂,否则走post 如果是数组的取第一个/最后一个,或者拼接成一个传给后端
    if (method === "get") {
      for (let key in query) {
        if (isArray(query[key]) && query[key].length) {
          query[key] = query[key][0] + "";
        } else {
          query[key] = query[key] + "";
        }
      }
    }
    let requestBody = this.sortAsc({ ...query, nonce, timestamp });
 
    return md5(JSON.stringify(requestBody)).toUpperCase();
  }
}

后端处理

首先取到post请求body内的内容

package cn.yscs.common.gateway.filter;

import io.netty.buffer.ByteBufAllocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.nio.charset.StandardCharsets;

/**
 * 获取请求体内的数据放入请求参数中
 *
 * @author 
 */
@Component
public class RequestBodyFilter implements GlobalFilter, Ordered {
    private final static Logger log = LoggerFactory.getLogger(RequestBodyFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (HttpMethod.POST.equals(exchange.getRequest().getMethod()) && null != exchange.getRequest().getHeaders().getContentType()
                && exchange.getRequest().getHeaders().getContentType().includes(MediaType.APPLICATION_JSON)
                && !exchange.getRequest().getHeaders().getContentType().includes(MediaType.MULTIPART_FORM_DATA)) {

            return DataBufferUtils.join(exchange.getRequest().getBody()).map(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                return bytes;
            }).flatMap(bodyBytes -> {
                String msg = new String(bodyBytes, StandardCharsets.UTF_8);
                exchange.getAttributes().put("CACHE_REQUEST_BODY", msg);
                return chain.filter(exchange.mutate().request(generateNewRequest(exchange.getRequest(), bodyBytes)).build());
            });
        }
        return chain.filter(exchange);
    }

    private ServerHttpRequest generateNewRequest(ServerHttpRequest request, byte[] bytes) {
        URI ex = UriComponentsBuilder.fromUri(request.getURI()).build(true).toUri();
        ServerHttpRequest newRequest = request.mutate().uri(ex).build();
        DataBuffer dataBuffer = stringBuffer(bytes);
        Flux<DataBuffer> flux = Flux.just(dataBuffer);
        newRequest = new ServerHttpRequestDecorator(newRequest) {
            @Override
            public Flux<DataBuffer> getBody() {
                return flux;
            }
        };
        return newRequest;
    }

    private DataBuffer stringBuffer(byte[] bytes) {
        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
        return nettyDataBufferFactory.wrap(bytes);
    }


    @Override
    public int getOrder() {
        return -5;
    }
}

然后继续在过滤器内验签,注意这个过滤器得在上面过滤器之后

package cn.yscs.common.gateway.filter;


import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.stgyl.scm.common.exception.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.DigestUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.*;


/**
 * api接口验签
 *
 * @author xuzhangxing
 */
public class ApiVerifyFilter implements GlobalFilter, Ordered {
    private final static Logger log = LoggerFactory.getLogger(ApiVerifyFilter.class);
    public static final String NONCE = "nonce";
    public static final String SIGNATURE = "signature";
    public static final String TIMESTAMP = "timestamp";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
        String requestId = httpHeaders.getFirst(NONCE);
        String requestSignature = httpHeaders.getFirst(SIGNATURE);
        String timestamp = httpHeaders.getFirst(TIMESTAMP);
        String path = exchange.getRequest().getURI().getRawPath();
        if (StrUtil.isBlank(requestId) || StrUtil.isBlank(requestSignature) || StrUtil.isBlank(timestamp)) {
            log.info("接口验签入参缺失requestId={} requestSignature={} timestamp={} path={}"
                    , requestId, requestSignature, timestamp,path);
            return Mono.error(() -> new ValidationException("接口验签失败!"));
        }

        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String method = serverHttpRequest.getMethodValue();
        SortedMap<String, String> encryptMap = new TreeMap<>();
        encryptMap.put(TIMESTAMP, timestamp);
        encryptMap.put(NONCE, requestId);
        encryptMap.put(SIGNATURE, requestSignature);

        if ("POST".equals(method)) {
            //从请求里获取Post请求体
            String requestBody = (String) exchange.getAttributes().get("CACHE_REQUEST_BODY");
            Map bodyParamMap = JSONObject.parseObject(requestBody, LinkedHashMap.class, Feature.OrderedField);
            if (CollUtil.isNotEmpty(bodyParamMap)) {
                encryptMap.putAll(bodyParamMap);
            }
            //封装request/传给下一级
            if (verifySign(encryptMap)) {
                return chain.filter(exchange);
            }
        } else if ("GET".equals(method) || "DELETE".equals(method)) {
            MultiValueMap<String, String> queryParams = serverHttpRequest.getQueryParams();
            if (CollUtil.isNotEmpty(queryParams)) {
                for (Map.Entry<String, List<String>> queryMap : queryParams.entrySet()) {
                    encryptMap.put(queryMap.getKey(), CollUtil.getFirst(queryMap.getValue()));
                }
            }
            //封装request/传给下一级
            if (verifySign(encryptMap)) {
                return chain.filter(exchange);
            }
        } else {
            return chain.filter(exchange);
        }
        log.info("接口验签失败请求url={}  map={}",path,encryptMap);
        return Mono.error(() -> new ValidationException("接口验签失败!!!"));
    }

    /**
     * @param params 参数都会在这里进行排序加密
     * @return 验证签名结果
     */
    public static boolean verifySign(SortedMap<String, String> params) {
        String urlSign = params.get(SIGNATURE);
        //把参数加密
        params.remove(SIGNATURE);
        String paramsJsonStr = JSONObject.toJSONString(params, SerializerFeature.WriteMapNullValue);
        String paramsSign = DigestUtils.md5DigestAsHex(paramsJsonStr.getBytes()).toUpperCase();
        boolean result = StrUtil.equals(urlSign, paramsSign);
        if (!result) {
            log.info("验签失败,系统计算的 Sign : {}    前端传递的 Sign : {} paramsJsonStr : {}", paramsSign, urlSign, paramsJsonStr);
        }
        return result;
    }


    @Override
    public int getOrder() {
        return 80;
    }
}注意点:

1、get请求入参不能太复杂,最好是单个参数的,如果是数组的注意统一处理

2、后端获取到请求参数后,注意字段为空的情况,默认JSONOject.toJSONString会忽略空

posted on 2022-11-29 15:56  _掌心  阅读(818)  评论(0编辑  收藏  举报