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会忽略空
gateway 网关接口防篡改验签的更多相关文章
- 支付宝支付集成中:refund_fastpay_by_platform_nopwd接口服务器通知验签不通过
在做p2p配资平台,也就是公司的项目,遇到了一个问题:refund_fastpay_by_platform_nopwd接口服务器通知验签不通过 下面是实录: 通知服务器的POST过来的数据: 1.si ...
- java安全入门篇之接口验签
文章大纲 一.加密与验签介绍二.接口验签实操三.项目源码下载 一.加密与验签介绍 大多数公共网络是不安全的,一切基于HTTP协议的请求/响应(Request or Response)都是可以被 ...
- SoapUI接口测试-验签值处理-调用java的加密jar包
转载自:https://www.jianshu.com/p/7c672426a165 一. 背景: 调用接口时有个请求参数是对请求入参按一定规则进行加密生成的验签值,每次不同参数的请求生成唯一的验签值 ...
- Java Http接口加签、验签操作方法
1.业务背景 最近接触了一些电商业务,发现在处理电商业务接口时,比如淘宝.支付类接口,接口双方为了确保数据参数在传输过程中未经过篡改,都需要对接口数据进行加签,然后在接口服务器端对接口参数进行验签,确 ...
- tomcat servlet JSP common gateway interface 公共网关接口
Tomcat主要充当servlet/JSP容器,不过它却有大量的功能可以与传统的Web服务器相媲美,对公共网关接口(Common Gateway Interface)的支持就是其中之一. 传统的Web ...
- Spring Boot如何设计防篡改、防重放攻击接口
Spring Boot 防篡改.防重放攻击 本示例要内容 请求参数防止篡改攻击 基于timestamp方案,防止重放攻击 使用swagger接口文档自动生成 API接口设计 API接口由于需要供第三方 ...
- Spring AOP实现接口验签
因项目需要与外部对接,为保证接口的安全性需要使用aop进行方法的验签; 在调用方法的时候,校验外部传入的参数进行验证, 验证通过就执行被调用的方法,验证失败返回错误信息: 不是所有的方法都需要进行验签 ...
- Spring Cloud实战 | 第十一篇:Spring Cloud Gateway 网关实现对RESTful接口权限控制和按钮权限控制
一. 前言 hi,大家好,这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT实现的统 ...
- 支付接口中常用的加密解密以及验签rsa,md5,sha
一.常用加密类型分类 1.对称加密:采用单钥对信息进行加密和解密,即同一个秘钥既可以对信息进行加密,也可以进行解密.此类型称之为对称加密.特点速度快,常用于对大量数据信息或文件加密时使用.常用例子:D ...
- CGI(Common Gateway Interface),通用网关接口
通用网关接口,简称CGI,是一种根据请求信息动态产生回应内容的技术.通过CGI,Web 服务器可以将根据请求不同启动不同的外部程序,并将请求内容转发给该程序,在程序执行结束后,将执行结果作为回应返回给 ...
随机推荐
- Dockerfile的指令和编写
每个优秀的人,背后都有一段沉默的时光 前言 学习Docker基础知识 什么是Dockerfile? Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明 指 ...
- 关于vue组件传值和事件绑定问题
<template> <view style="width: 100%; height: 100%;"> <view class="tabs ...
- Hanlp 在Python环境中安装、介绍及使用
Hanlp HanLP是由一系列模型与算法组成的Java工具包,目标是普及自然语言处理在生产环境中的应用.HanLP具备功能完善.性能高效.架构清晰.语料时新.可自定义的特点. 功能:中文分词 词性标 ...
- 多个pie环形图 逆时针旋转
效果图如下 代码如下 data = [ { name: "经济目的", value: 754, }, { name: "网络安全爱好者", value: 61 ...
- 基于AD9361的双收双发射频FMC子卡
FMC177-基于AD9361的双收双发射频FMC子卡 一.板卡介绍 FMC177射频模块分别包含两个接收通道与发射通道,其频率可覆盖达到70MHz~6GHz,AD9361芯片提供具有成本效益的实验平 ...
- 退役*CPCer的找实习总结
从2月底开始到今天,我终于拿到了第一个也是唯一一个offer(字节跳动).找实习的过程告一段落,所以想记录一下这段时间的经历. 最开始找$meopass$学长内推了小马智行,很快就接到了面试通知(再次 ...
- ES6的总结的一些数组、字符串方法
1.数组的方法 unshift() 数组头部添加内容 push() 数组尾部添加内容 pop() 数组尾部删除内容 shift() 数组头部删除内容 sort() 数组排序 a-b 升序 b-a 降序 ...
- Bugku-不可破译的密码[wp]
一 题目分析 flag.txt cipher.txt (1)密码表形式和维吉尼亚密码一样 (2)看到504Q0304 很容易想到 504B0304 Zip文件头. 二 解题步骤 2.1 解密密文 根据 ...
- js网页禁止右键下载代码
<script type="text/javascript"> //禁用右键 document.onkeydown = function() { var e = win ...
- 硬件IIC的重映射使用问题
目录 沁恒的蓝牙系列芯片,有映射硬件模块去其他引脚的功能,可以配置各芯片的功能引脚重映射寄存器(R16_PIN_ALTERNATE),或者使用函数GPIOPinRemap函数进行配置. 比如说想要配置 ...