|
|
|
@ -0,0 +1,239 @@
|
|
|
|
|
package cn.iocoder.yudao.gateway.filter.front;
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.codec.Base64Encoder;
|
|
|
|
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
|
|
import cn.iocoder.yudao.gateway.util.SecurityFrameworkUtils;
|
|
|
|
|
import cn.iocoder.yudao.gateway.util.secret.AESUtils;
|
|
|
|
|
import cn.iocoder.yudao.gateway.util.secret.RSAUtils;
|
|
|
|
|
import com.alibaba.nacos.common.utils.StringUtils;
|
|
|
|
|
import jakarta.annotation.Resource;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.reactivestreams.Publisher;
|
|
|
|
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
|
|
|
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
|
|
|
|
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
|
|
|
|
|
import org.springframework.cloud.gateway.support.BodyInserterContext;
|
|
|
|
|
import org.springframework.cloud.gateway.support.NotFoundException;
|
|
|
|
|
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
|
|
|
|
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.core.io.buffer.DefaultDataBufferFactory;
|
|
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
|
|
import org.springframework.http.HttpMethod;
|
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
|
import org.springframework.http.ReactiveHttpOutputMessage;
|
|
|
|
|
import org.springframework.http.codec.CodecConfigurer;
|
|
|
|
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
|
|
|
|
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
|
|
|
|
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
|
|
|
|
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
|
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
import org.springframework.web.reactive.function.BodyInserter;
|
|
|
|
|
import org.springframework.web.reactive.function.BodyInserters;
|
|
|
|
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
|
|
|
|
import org.springframework.web.server.ServerWebExchange;
|
|
|
|
|
import reactor.core.publisher.Flux;
|
|
|
|
|
import reactor.core.publisher.Mono;
|
|
|
|
|
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
|
|
|
@Slf4j
|
|
|
|
|
@Component
|
|
|
|
|
public class FrontSecretFilter implements GlobalFilter, Ordered {
|
|
|
|
|
@Resource
|
|
|
|
|
private ParamterSecretProperties secretProperties;
|
|
|
|
|
|
|
|
|
|
@Resource
|
|
|
|
|
private CodecConfigurer codecConfigurer;
|
|
|
|
|
|
|
|
|
|
private String aesKey;
|
|
|
|
|
private BodyDto bodyDto=new BodyDto();
|
|
|
|
|
@Override
|
|
|
|
|
public int getOrder() {
|
|
|
|
|
return Ordered.HIGHEST_PRECEDENCE+98; //在日志之前,以免日志无法打印
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
|
|
|
|
// 将 Request 中可以直接获取到的参数,设置到网关日志
|
|
|
|
|
ServerHttpRequest request = exchange.getRequest();
|
|
|
|
|
|
|
|
|
|
//筛选方法,只对get put post处理
|
|
|
|
|
String method = request.getMethod().name();
|
|
|
|
|
if (request.getMethod() != HttpMethod.POST && request.getMethod() != HttpMethod.PUT && request.getMethod() != HttpMethod.PATCH && request.getMethod() != HttpMethod.GET) {
|
|
|
|
|
// 如果不是post(新增)、put(全量修改)、patch(部分字段修改)GET(加密返回体) 操作,则直接放行
|
|
|
|
|
return chain.filter(exchange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String secretHeader = request.getHeaders().getFirst(secretProperties.getHeader());
|
|
|
|
|
if(StrUtil.isEmpty(secretHeader)){ //无头部指示
|
|
|
|
|
return chain.filter(exchange);
|
|
|
|
|
}
|
|
|
|
|
if(!secretHeader.equals("true")) //头部指示不为true
|
|
|
|
|
return chain.filter(exchange);
|
|
|
|
|
if(!secretProperties.getEnable()) //系统配置为前后端不加密
|
|
|
|
|
return chain.filter(exchange);
|
|
|
|
|
for(String url : secretProperties.getIgnoreUrls()){ //白名单剔除
|
|
|
|
|
if(request.getURI().getPath().contains(url))
|
|
|
|
|
return chain.filter(exchange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aesKey = exchange.getRequest().getHeaders().getFirst("frontSecKEY");//前端通过header 传递的aes key
|
|
|
|
|
try {
|
|
|
|
|
//使用rsa解密 key
|
|
|
|
|
aesKey = RSAUtils.decrypt(aesKey, secretProperties.getPrivateKey());
|
|
|
|
|
}catch (Exception e){
|
|
|
|
|
log.error("解密前端密钥失败:"+e.getMessage());
|
|
|
|
|
throw NotFoundException.create(true, "Unable to find instance for " + request.getURI().getHost());
|
|
|
|
|
}
|
|
|
|
|
// 继续 filter 过滤
|
|
|
|
|
if(method.equals(HttpMethod.POST.name()) || method.equals(HttpMethod.PUT.name())) {
|
|
|
|
|
return filterWithRequestBody(exchange, chain);
|
|
|
|
|
}
|
|
|
|
|
return filterWithoutRequestBody(exchange, chain); //get method 不拦截request
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain) {
|
|
|
|
|
// 设置 Request Body 读取时,设置到网关日志
|
|
|
|
|
// 此处 codecConfigurer.getReaders() 的目的,是解决 spring.codec.max-in-memory-size 不生效
|
|
|
|
|
ServerRequest serverRequest = ServerRequest.create(exchange, codecConfigurer.getReaders());
|
|
|
|
|
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
|
|
|
|
|
//**解码
|
|
|
|
|
try {
|
|
|
|
|
String _body = body.substring(1, body.length() - 1);
|
|
|
|
|
String originalQuery = AESUtils.decrypt(_body, aesKey);
|
|
|
|
|
|
|
|
|
|
bodyDto.setRequestBody(originalQuery);
|
|
|
|
|
return Mono.just(originalQuery);
|
|
|
|
|
}catch (Exception e){
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return Mono.just(body);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 BodyInserter 对象
|
|
|
|
|
BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
|
|
|
|
|
// 创建 CachedBodyOutputMessage 对象
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
headers.putAll(exchange.getRequest().getHeaders());
|
|
|
|
|
// the new content type will be computed by bodyInserter
|
|
|
|
|
// and then set in the request decorator
|
|
|
|
|
headers.remove(HttpHeaders.CONTENT_LENGTH); // 移除
|
|
|
|
|
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
|
|
|
|
|
// 通过 BodyInserter 将 Request Body 写入到 CachedBodyOutputMessage 中
|
|
|
|
|
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
|
|
|
|
|
// 包装 Request,用于缓存 Request Body
|
|
|
|
|
ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
|
|
|
|
|
// 包装 Response,用于记录 Response Body
|
|
|
|
|
ServerHttpResponseDecorator decoratedResponse = encodeResponse(exchange, bodyDto);
|
|
|
|
|
return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build());
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加密响应包
|
|
|
|
|
* 通过 DataBufferFactory 解决响应体分段传输问题。
|
|
|
|
|
* @param exchange
|
|
|
|
|
* @param bodyDto
|
|
|
|
|
* @return
|
|
|
|
|
*/
|
|
|
|
|
private ServerHttpResponseDecorator encodeResponse(ServerWebExchange exchange, BodyDto bodyDto) {
|
|
|
|
|
ServerHttpResponse response = exchange.getResponse();
|
|
|
|
|
return new ServerHttpResponseDecorator(response) {
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
|
|
|
|
if (body instanceof Flux) {
|
|
|
|
|
DataBufferFactory bufferFactory = response.bufferFactory();
|
|
|
|
|
// 获取响应类型,如果是 json 就打印
|
|
|
|
|
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
|
|
|
|
|
if (StringUtils.isNotBlank(originalResponseContentType)
|
|
|
|
|
&& originalResponseContentType.contains("application/json")) {
|
|
|
|
|
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
|
|
|
|
|
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
|
|
|
|
|
// 设置 response body 到网关日志
|
|
|
|
|
byte[] content = readContent(dataBuffers);
|
|
|
|
|
String responseResult = new String(content, StandardCharsets.UTF_8);
|
|
|
|
|
//加密
|
|
|
|
|
bodyDto.setResponseBody(responseResult);
|
|
|
|
|
log.info("aesKey:"+aesKey);
|
|
|
|
|
String _content = AESUtils.encrypt(responseResult, (aesKey));
|
|
|
|
|
log.info("响应整体加密:"+_content);
|
|
|
|
|
// 响应
|
|
|
|
|
return bufferFactory.wrap(_content.getBytes());
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// if body is not a flux. never got there.
|
|
|
|
|
return super.writeWith(body);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public HttpHeaders getHeaders() {
|
|
|
|
|
HttpHeaders httpHeaders = getDelegate().getHeaders();
|
|
|
|
|
// if(!httpHeaders.containsKey(secretProperties.getHeader())){ //此处不写也是可以的,在前端可以通过response.requst得到
|
|
|
|
|
// httpHeaders.add(secretProperties.getHeader(), "true");
|
|
|
|
|
// }
|
|
|
|
|
return httpHeaders;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain) {
|
|
|
|
|
// 包装 Response,用于记录 Response Body
|
|
|
|
|
ServerHttpResponseDecorator decoratedResponse = encodeResponse(exchange, bodyDto);
|
|
|
|
|
return chain.filter(exchange.mutate().response(decoratedResponse).build());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 参考 ModifyResponseBodyGatewayFilterFactory 中的方法 ==========
|
|
|
|
|
|
|
|
|
|
private byte[] readContent(List<? extends DataBuffer> dataBuffers) {
|
|
|
|
|
// 合并多个流集合,解决返回体分段传输
|
|
|
|
|
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
|
|
|
|
|
DataBuffer join = dataBufferFactory.join(dataBuffers);
|
|
|
|
|
byte[] content = new byte[join.readableByteCount()];
|
|
|
|
|
join.read(content);
|
|
|
|
|
// 释放掉内存
|
|
|
|
|
DataBufferUtils.release(join);
|
|
|
|
|
return content;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 请求装饰器,支持重新计算 headers、body 缓存
|
|
|
|
|
*
|
|
|
|
|
* @param exchange 请求
|
|
|
|
|
* @param headers 请求头
|
|
|
|
|
* @param outputMessage body 缓存
|
|
|
|
|
* @return 请求装饰器
|
|
|
|
|
*/
|
|
|
|
|
private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
|
|
|
|
|
return new ServerHttpRequestDecorator(exchange.getRequest()) {
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public HttpHeaders getHeaders() {
|
|
|
|
|
long contentLength = headers.getContentLength();
|
|
|
|
|
HttpHeaders httpHeaders = new HttpHeaders();
|
|
|
|
|
httpHeaders.putAll(super.getHeaders());
|
|
|
|
|
if (contentLength > 0) {
|
|
|
|
|
httpHeaders.setContentLength(contentLength);
|
|
|
|
|
} else {
|
|
|
|
|
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on
|
|
|
|
|
// httpbin.org
|
|
|
|
|
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
|
|
|
|
|
}
|
|
|
|
|
return httpHeaders;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public Flux<DataBuffer> getBody() {
|
|
|
|
|
return outputMessage.getBody();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|