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.slf4j.Logger; import org.slf4j.LoggerFactory; 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 org.springframework.web.server.WebSession; import org.springframework.web.server.session.WebSessionManager; 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 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 unauthorizedResponse(ServerWebExchange exchange, String msg) { log.error("[签名异常处理]请求路径:{}", exchange.getRequest().getPath()); return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.BAD_REQUEST); } /** * multipart请求的签名校验(不读取文件流) * @return */ private Mono 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 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); log.info("请求参数:{}", bodyString); if(!authSign(JSON.parseObject(bodyString), sign, nonce, timestamp)){ return unauthorizedResponse(exchange, "请求失败,请稍后重试!"); } DataBufferUtils.release(dataBuffer); Flux cachedFlux = Flux.defer(() -> { DataBuffer buffer = exchange.getResponse().bufferFactory() .wrap(bytes); return Mono.just(buffer); }); ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux 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 keySet = new ArrayList<>(params.keySet()); // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) Collections.sort(keySet, new Comparator() { @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; log.info("签名串:" + signUrl); return signUrlEncode(signUrl, key); } /** * 签名校验 * * @return */ private boolean authSign(JSONObject jsonStr, String sign, String nonce, String timestamp) { log.info("请求签名:" + sign); String signUrlEncode = localSignUrl(jsonStr, nonce, timestamp); signUrlEncode = signUrlEncode.replaceAll("& #40;", "\\(") .replaceAll("& #41;", "\\)"); log.info("签名key:{}", nonce); log.info("校验签名值:" + signUrlEncode); 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; } }