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会忽略空

gateway 网关接口防篡改验签的更多相关文章

  1. 支付宝支付集成中:refund_fastpay_by_platform_nopwd接口服务器通知验签不通过

    在做p2p配资平台,也就是公司的项目,遇到了一个问题:refund_fastpay_by_platform_nopwd接口服务器通知验签不通过 下面是实录: 通知服务器的POST过来的数据: 1.si ...

  2. java安全入门篇之接口验签

    文章大纲 一.加密与验签介绍二.接口验签实操三.项目源码下载   一.加密与验签介绍   大多数公共网络是不安全的,一切基于HTTP协议的请求/响应(Request or Response)都是可以被 ...

  3. SoapUI接口测试-验签值处理-调用java的加密jar包

    转载自:https://www.jianshu.com/p/7c672426a165 一. 背景: 调用接口时有个请求参数是对请求入参按一定规则进行加密生成的验签值,每次不同参数的请求生成唯一的验签值 ...

  4. Java Http接口加签、验签操作方法

    1.业务背景 最近接触了一些电商业务,发现在处理电商业务接口时,比如淘宝.支付类接口,接口双方为了确保数据参数在传输过程中未经过篡改,都需要对接口数据进行加签,然后在接口服务器端对接口参数进行验签,确 ...

  5. tomcat servlet JSP common gateway interface 公共网关接口

    Tomcat主要充当servlet/JSP容器,不过它却有大量的功能可以与传统的Web服务器相媲美,对公共网关接口(Common Gateway Interface)的支持就是其中之一. 传统的Web ...

  6. Spring Boot如何设计防篡改、防重放攻击接口

    Spring Boot 防篡改.防重放攻击 本示例要内容 请求参数防止篡改攻击 基于timestamp方案,防止重放攻击 使用swagger接口文档自动生成 API接口设计 API接口由于需要供第三方 ...

  7. Spring AOP实现接口验签

    因项目需要与外部对接,为保证接口的安全性需要使用aop进行方法的验签; 在调用方法的时候,校验外部传入的参数进行验证, 验证通过就执行被调用的方法,验证失败返回错误信息: 不是所有的方法都需要进行验签 ...

  8. Spring Cloud实战 | 第十一篇:Spring Cloud Gateway 网关实现对RESTful接口权限控制和按钮权限控制

    一. 前言 hi,大家好,这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT实现的统 ...

  9. 支付接口中常用的加密解密以及验签rsa,md5,sha

    一.常用加密类型分类 1.对称加密:采用单钥对信息进行加密和解密,即同一个秘钥既可以对信息进行加密,也可以进行解密.此类型称之为对称加密.特点速度快,常用于对大量数据信息或文件加密时使用.常用例子:D ...

  10. CGI(Common Gateway Interface),通用网关接口

    通用网关接口,简称CGI,是一种根据请求信息动态产生回应内容的技术.通过CGI,Web 服务器可以将根据请求不同启动不同的外部程序,并将请求内容转发给该程序,在程序执行结束后,将执行结果作为回应返回给 ...

随机推荐

  1. php 允许跨域

    1.控制器 header("Access-Control-Allow-Origin: *"); class Index extends Api {} 2.app/admin/con ...

  2. [JavaScript]对象数组 - 不完全整理

    对象数组中查询属性为某个值的对象,使用Array.find() const array1 = [5, 12, 8, 130, 44]; const found = array1.find(elemen ...

  3. JavaScript项目榜单

    JavaScript项目榜单 参考资料 2022年最受欢迎的JavaScript项目榜单出炉 Best of JS 正式公布 2022 年 JavaScript 明星项目榜单 该榜单提供了过去 12 ...

  4. Dapper、EF、WebAPI转载记录

    轻量级框架Dapper基础 https://www.cnblogs.com/Sinte-Beuve/p/4231053.html   基本使用 https://www.cnblogs.com/hxzb ...

  5. 解决Mac安装Homebrew失败

    首先使用Homebrew官网的安装shell命令安装: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebr ...

  6. C3861: “CoInitialize”“CoUninitialize”: 找不到标识符

    error C3861: "CoInitialize": 找不到标识符error C3861: "CoUninitialize": 找不到标识符 包含头文件和链 ...

  7. Java-根据父级id将List结构转Tree结构

    List的stream public ResultData queryMenuList() { // 获取所有数据List List<MenuVo> list = MenuDao.quer ...

  8. webpack 5 配置babel-loader babel7

    1.安装Balel目的: 在webpack中 默认只能处理部分 ES6的新语法,一些更高级的ES6或ES7的语法,webpack是处理不了的这个时候就需要借助第三方的loader 来帮助webpack ...

  9. java将Word转换成PDF方法

    转载1:java将Word转换成PDF三种方法_pdfoptions_Zhsh-7的博客-CSDN博客 转载2:POI 实现 word转成pdf - 挽留匆匆的美丽 - 博客园 (cnblogs.co ...

  10. 【驱动 】frambuffer中显示屏参数的修改

    1.在x210板子的kernel中,默认LCD显示屏是800*400的,修改在 kernel/arch/arm/mach-s5pv210/mach-x210.c 中 258行 #define S5PV ...