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会忽略空
标签:网关,return,String,exchange,springframework,org,验签,import,gateway From: https://www.cnblogs.com/xuzhangx/p/16935611.html