最近公司的项目需要和某付宝进行接口对接,需要用到网关进行鉴权操作,并且需要把验证签名的操作也提取到网关层,减少代码的冗余。在开发过程中遇到了主要问题是:
某付宝的接口虽然是使用post请求,但是参数是挂载在query上(http请求分为三处携带数据 header、query和body),这样的话当请求进入网关层会存在参数加密解密的问题,度娘了一圈没有发现类似情况,莫非只是我的特例嘛?.....然而当鉴权完毕转发到业务层处理时,又会涉及到加密解密的问题,对于参数中的 “=”、“空格”等会被识别为非法字符。
折腾了好久,最后抱着试一试的态度竟然奇迹的成功了,期间出过各种坚决方案,但基本都是扶起来东墙,西墙又倒了。直接上代码先看下:
import com.alibaba.fastjson.JSON;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
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.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
/**
* aikes 20201014
* 网关过滤器
*/
@Component
public class AlipayFilterA implements GlobalFilter, Ordered {
private static Logger logger = LoggerFactory.getLogger(AlipayFilterA.class);
@Value("${sign.sign_type}")
private String sign_type;
@Value("${sign.app_id}")
private String app_id;
@Value("${sign.format}")
private String format;
@Value("${sign.charset}")
private String charset;
@Value("${sign.PUBLIC_KEY}")
private String PUBLIC_KEY;
@Value("${sign.PRIVATE_KEY}")
private String PRIVATE_KEY;
@Value("${sign.ALIPAY_PUBLIC_KEY}")
private String ALIPAY_PUBLIC_KEY;
@Value("${sign.version}")
private String VERSION;
@Override
public int getOrder() {
return -20;
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpResponse serverHttpResponse = exchange.getResponse();
StringBuilder logBuilder = new StringBuilder();
Map params = parseRequest(exchange, logBuilder);
logger.info("请求信息:" + logBuilder);
Map tResult = checkSignature(params);//参数校验
if (!(boolean) tResult.get("flag")) {
logger.info("请求参数有误,访问被拒绝!");
String resp = JSON.toJSONString(tResult.get("response"));
logBuilder.append(",resp=").append(resp);
DataBuffer bodyDataBuffer = serverHttpResponse.bufferFactory().wrap(resp.getBytes());
serverHttpResponse.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
return serverHttpResponse.writeWith(Mono.just(bodyDataBuffer));
}
DataBufferFactory bufferFactory = serverHttpResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(serverHttpResponse) {
@Override
public Mono writeWith(Publisher extends DataBuffer> body) {
if (body instanceof Flux) {
Flux extends DataBuffer> fluxBody = (Flux extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
String resp = new String(content, Charset.forName("UTF-8"));
logBuilder.append(",resp=").append(resp);
logger.info(logBuilder.toString());
byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
};
// 仅对请求参数加密处理
URI uri = serverHttpRequest.getURI();
StringBuilder query = new StringBuilder();
params.forEach((k, v) -> {
query.append(k);
query.append("=");
try {
query.append(URLEncoder.encode(v, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
query.append("&");
});
// 替换参数
URI newUri = UriComponentsBuilder.fromUri(uri)
.replaceQuery(query.substring(0,query.length()-1))
.build(true)
.toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
return chain.filter(exchange.mutate().request(request).build());
}
private Map checkSignature(Map params) {
//做参数校验
return null;
}
private Map parseRequest(ServerWebExchange exchange, StringBuilder logBuilder) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String method = serverHttpRequest.getMethodValue().toUpperCase();
logBuilder.append(method).append(",").append(serverHttpRequest.getURI());
MultiValueMap query = serverHttpRequest.getQueryParams();
logger.info("QueryParam 列表:" + JSON.toJSONString(query));
Map params = new HashMap<>();
query.forEach((k, v) -> {
params.put(k, v.get(0));
});
return params;
}
}
当前版本也只是暂时的解决方案,后续还需要细化打磨。关于过滤器的基本知识和依赖配置等这里就不进行说明了,网上的大佬们有很多详细介绍,我选最核心的部分解释下问题处理思路,也就是下方这块在重写的filter方法中最下面的一段代码。
// 仅对请求参数加密处理
URI uri = serverHttpRequest.getURI();
StringBuilder query = new StringBuilder();
params.forEach((k, v) -> {
query.append(k);
query.append("=");
try {
query.append(URLEncoder.encode(v, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
query.append("&");
});
// 替换参数
URI newUri = UriComponentsBuilder.fromUri(uri)
.replaceQuery(query.substring(0,query.length()-1))
.build(true)
.toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
return chain.filter(exchange.mutate().request(request).build());
这段代码的主要作用就是针对http请求中query部分的参数进行加密操作,加密完成后,通过替换参数的方式获取到新的URI对象,然后再将生成的URI对象装载到copy出来的ServerHttpRequest对象中,最后通过过滤器链将请求流转下去。
后续就会根据网关层的配置进行请求转发到对应的业务处理层的微服务中,匹配到对应的路径方法下,通过@RequestParam Map