package com.xiaopeng.gateway.filter;
|
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONObject;
|
import com.xiaopeng.common.core.constant.HttpStatus;
|
import com.xiaopeng.common.core.constant.TokenConstants;
|
import com.xiaopeng.common.core.utils.ServletUtils;
|
import com.xiaopeng.common.core.utils.StringUtils;
|
import com.xiaopeng.common.redis.service.RedisService;
|
import com.xiaopeng.gateway.config.properties.IgnoreWhiteProperties;
|
import com.xiaopeng.gateway.config.properties.SignProperties;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.codec.binary.Base64;
|
import org.springframework.beans.factory.annotation.Autowired;
|
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.http.HttpCookie;
|
import org.springframework.http.HttpMethod;
|
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 reactor.core.publisher.Flux;
|
import reactor.core.publisher.Mono;
|
|
import javax.annotation.Resource;
|
import java.nio.charset.StandardCharsets;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.Comparator;
|
import java.util.List;
|
import java.util.concurrent.TimeUnit;
|
|
/**
|
* 网关鉴权
|
*
|
* @author xiaopeng
|
*/
|
@Slf4j
|
@Component
|
public class SignFilter implements GlobalFilter, Ordered {
|
|
@Resource
|
private IgnoreWhiteProperties ignoreWhite;
|
|
@Resource
|
private SignProperties signProperties;
|
|
@Resource
|
private RedisService redisService;
|
|
|
@Override
|
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
ServerHttpRequest request = exchange.getRequest();
|
ServerHttpRequest.Builder mutate = request.mutate();
|
|
HttpMethod method = request.getMethod();
|
if(method != HttpMethod.POST){
|
return chain.filter(exchange.mutate().request(mutate.build()).build());
|
}
|
|
// 跳过不需要验证的路径
|
String url = request.getURI().getPath();
|
if (StringUtils.matches(url, signProperties.getWhites())) {
|
return chain.filter(exchange);
|
}
|
|
// 判断是否为multipart请求
|
String contentType = request.getHeaders().getFirst("Content-Type");
|
boolean isMultipart = contentType != null && contentType.toLowerCase().contains("multipart/");
|
|
|
//校验签名
|
if(signProperties.getEnable()){
|
String sign = request.getHeaders().getFirst(TokenConstants.SIGN);
|
String timestamp = request.getHeaders().getFirst(TokenConstants.TIMESTAMP);
|
String nonce = request.getHeaders().getFirst(TokenConstants.NONCE);
|
//签名校验开关打开后校验必传参数是否有效
|
if((StringUtils.isEmpty(sign) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(nonce))){
|
return unauthorizedResponse(exchange, "请求失败,请稍后重试!");
|
}
|
//校验请求是否过期
|
if(System.currentTimeMillis() - Long.parseLong(timestamp) > TokenConstants.EXPIRATION_TIME * 1000){
|
return unauthorizedResponse(exchange, "请求失败,请稍后重试!");
|
}
|
//校验请求是否重复提交
|
Boolean hasKey = redisService.hasKey("sign:nonce:" + nonce);
|
if(hasKey){
|
return unauthorizedResponse(exchange, "请求失败,请稍后重试!");
|
}
|
//数据缓存保留24小时
|
redisService.setCacheObject("sign:nonce:" + nonce, nonce, 24L, TimeUnit.DAYS);
|
// 对于multipart请求,只校验headers,不读取body
|
if(isMultipart){
|
return authMultipartSign(exchange, chain, sign, nonce, timestamp);
|
} else {
|
return authSign(exchange, chain, sign, nonce, timestamp);
|
}
|
}
|
return chain.filter(exchange.mutate().request(mutate.build()).build());
|
}
|
|
|
|
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
|
log.error("[签名异常处理]请求路径:{}", exchange.getRequest().getPath());
|
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.BAD_REQUEST);
|
}
|
|
/**
|
* multipart请求的签名校验(不读取文件流)
|
* @return
|
*/
|
private Mono<Void> authMultipartSign(ServerWebExchange exchange, GatewayFilterChain chain, String sign, String nonce, String timestamp){
|
// 对于multipart请求,只使用headers信息进行签名验证
|
// 构造基于headers的签名字符串
|
JSONObject headerParams = new JSONObject();
|
headerParams.put("content-type", exchange.getRequest().getHeaders().getFirst("Content-Type"));
|
headerParams.put("content-length", exchange.getRequest().getHeaders().getFirst("Content-Length"));
|
if(!authSign(headerParams, sign, nonce, timestamp)){
|
return unauthorizedResponse(exchange, "请求失败,请稍后重试!");
|
}
|
// 直接放行,不读取body
|
return chain.filter(exchange);
|
}
|
|
/**
|
* 校验签名
|
* @return
|
*/
|
private Mono<Void> authSign(ServerWebExchange exchange, GatewayFilterChain chain, String sign, String nonce, String timestamp){
|
return DataBufferUtils.join(exchange.getRequest().getBody())
|
.flatMap(dataBuffer -> {
|
byte[] bytes = new byte[dataBuffer.readableByteCount()];
|
dataBuffer.read(bytes);
|
String bodyString = new String(bytes, StandardCharsets.UTF_8);
|
if(!authSign(JSON.parseObject(bodyString), sign, nonce, timestamp)){
|
return unauthorizedResponse(exchange, "请求失败,请稍后重试!");
|
}
|
DataBufferUtils.release(dataBuffer);
|
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
|
DataBuffer buffer = exchange.getResponse().bufferFactory()
|
.wrap(bytes);
|
return Mono.just(buffer);
|
});
|
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
|
@Override
|
public Flux<DataBuffer> getBody() {
|
return cachedFlux;
|
}
|
};
|
return chain.filter(exchange.mutate().request(mutatedRequest)
|
.build());
|
});
|
}
|
|
/**
|
* 组装签名路径
|
* @param params
|
* @return
|
*/
|
public static String localSignUrl(JSONObject params, String key, String timestamp) {
|
List<String> keySet = new ArrayList<>(params.keySet());
|
// 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序)
|
Collections.sort(keySet, new Comparator<String>() {
|
@Override
|
public int compare(String o1, String o2) {
|
return o1.compareTo(o2);
|
}
|
});
|
// 构造签名键值对的格式
|
StringBuilder sb = new StringBuilder();
|
for (String k : keySet) {
|
Object o = params.get(k);
|
if(o instanceof JSONObject || o instanceof JSONArray){
|
continue;
|
}
|
String v = params.getString(k);
|
if(StringUtils.isNotEmpty(v)){
|
sb.append(k + "=" + v + "&");
|
}
|
}
|
String signUrl = "";
|
if(sb.length() != 0){
|
signUrl = sb.substring(0, sb.length() - 1);
|
}
|
signUrl = signUrl + (StringUtils.isNotEmpty(signUrl) ? "&" : "") + "timestamp=" + timestamp;
|
return signUrlEncode(signUrl, key);
|
}
|
|
/**
|
* 签名校验
|
*
|
* @return
|
*/
|
private boolean authSign(JSONObject jsonStr, String sign, String nonce, String timestamp) {
|
String signUrlEncode = localSignUrl(jsonStr, nonce, timestamp);
|
signUrlEncode = signUrlEncode.replaceAll("& #40;", "\\(")
|
.replaceAll("& #41;", "\\)");
|
if (sign.equals(signUrlEncode)) {
|
return true;
|
}
|
return false;
|
}
|
|
|
/**
|
* 签名字符串加密
|
* @param signUrl
|
* @param encryptKey
|
* @return
|
*/
|
public static String signUrlEncode(String signUrl, String encryptKey) {
|
String localSign = HmacSHA256Util.hmacSha256Base64(signUrl, encryptKey);
|
return localSign;
|
}
|
|
|
@Override
|
public int getOrder()
|
{
|
return -200;
|
}
|
}
|