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 服务器可以将根据请求不同启动不同的外部程序,并将请求内容转发给该程序,在程序执行结束后,将执行结果作为回应返回给 ...
随机推荐
- USACO2023Jan游记
由于学校要求,过来打 USACO. 似乎要求起码升到白金? 由于是第一次打,只有从铜组开始了. Brouze 简单题,就给核心代码. 30min AK. Leaders http://usaco.or ...
- 42.Linux查看日志的几种方式
Linux查看日志的命令有多种: tail.cat.tac.head.echo等,本文只介绍几种常用的方法. 1.tail 这个是我最常用的一种查看方式 命令格式: tail[必要参数][选择参数][ ...
- 第四周作业-N67044-张铭扬
1. 自定义写出10个定时任务的示例:比如每周三凌晨三点执行data命令要求尽量的覆盖各种场景 1)每天早上8点对磁盘使用率进行查看,若超出空间的80%,则发邮件报警 [root@centos8 ~] ...
- Day 13 13.1 refer反爬
Referer 一.referer是什么: 图片防盗链的技术应该还有其他的,目前了解到的是浏览器的referer,其实这是错误的拼写,正确是应该是referrer.不过现在可以看到Chrome的开发者 ...
- FMC DA子卡设计原理图:FMCJ465-2路 16bit 12.6GSPS FMC DA子卡
FMCJ465-2路 16bit 12.6GSPS FMC DA子卡 一.板卡概述: FMCJ465是一款转换速率最高为12.6GSPS 的 DAC 回放板,DAC位数16bit; 板卡基于 ...
- java报错 SLF4J:Failed to load class "org.slf4j.impl.StaticLoggerBinder"
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artif ...
- virtualBox虚拟机中安装linux系统并连接
1.安装virtualBox 1.1.下载 virtualBox是免费的,直接去官网下载 https://www.virtualbox.org/ 在官网左侧找到Downloads,下载需要的版本 1. ...
- 新手入门Neo4j,手把手完整教学
1. 图数据库Neo4j简介 1.1 什么是图数据库 图数据库:是基于图论实现的一种NoSQL数据库,其数据结构是和查询方式是以图论为基础的,图数据库主要用于存储更多的连接数据. 图论:用多个节点代表 ...
- Python基础前言
计算机内部存储数据的原理 """计算机内部只认识01二进制"""是因为计算机是基于电工作的,而电是有高低电频之分00000001 000 ...
- Kubernetes--Pod对象的生命周期
Pod对象自从其创建开始至其终止退出的时间范围称为其生命周期.在这段时间中,Pod会处于多种不同的状态,并执行一些操作:其中,创建主容器(main container)为必需的操作,其他可选的操作还包 ...