前後端request body response body加解密,安全整改
parent
fb355064d8
commit
68d7572391
|
|
@ -1,9 +1,12 @@
|
||||||
package cn.iocoder.yudao.gateway;
|
package cn.iocoder.yudao.gateway;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.gateway.filter.front.ParamterSecretProperties;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableConfigurationProperties(ParamterSecretProperties.class)
|
||||||
public class GatewayServerApplication {
|
public class GatewayServerApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import java.io.Serializable;
|
||||||
* @author feng
|
* @author feng
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class ResponseDto implements Serializable {
|
public class BodyDto implements Serializable {
|
||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = -6354714160436354659L;
|
private static final long serialVersionUID = -6354714160436354659L;
|
||||||
|
|
||||||
|
|
@ -20,4 +20,8 @@ public class ResponseDto implements Serializable {
|
||||||
private String msg;
|
private String msg;
|
||||||
|
|
||||||
private Object data;
|
private Object data;
|
||||||
|
|
||||||
|
private String requestBody;
|
||||||
|
|
||||||
|
private String responseBody;
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package cn.iocoder.yudao.gateway.filter.front;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数加密配置属性类 gareway globalfilter没起作用
|
||||||
|
* 请求需要带上 ENCPARAMTER=true 的请求头,才会进行参数加密
|
||||||
|
* @author atuchina
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties(prefix = "yudao.front.secret")
|
||||||
|
@Data
|
||||||
|
public class ParamterSecretProperties {
|
||||||
|
/**
|
||||||
|
* 参数加是否开启
|
||||||
|
*/
|
||||||
|
private static final Boolean ENABLE_DEFAULT = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启
|
||||||
|
*/
|
||||||
|
private Boolean enable = ENABLE_DEFAULT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要忽略参数加密的请求
|
||||||
|
*
|
||||||
|
* 这里可配置部分请求是不需要加密的
|
||||||
|
*/
|
||||||
|
private Set<String> ignoreUrls = Collections.emptySet();
|
||||||
|
|
||||||
|
private String header = "ENCPARAMTER"; // 请求头名称
|
||||||
|
|
||||||
|
@NotNull(message = "前后端参数加密 密钥 不能为空")
|
||||||
|
private String privateKey; // 加密秘钥
|
||||||
|
}
|
||||||
|
|
@ -98,7 +98,7 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getOrder() {
|
public int getOrder() {
|
||||||
return Ordered.HIGHEST_PRECEDENCE;
|
return Ordered.HIGHEST_PRECEDENCE+99;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public class UserSaveReqVO {
|
||||||
|
|
||||||
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
|
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
|
||||||
@NotBlank(message = "用户账号不能为空")
|
@NotBlank(message = "用户账号不能为空")
|
||||||
@Pattern(regexp = "^[a-zA-Z0-9]$", message = "用户账号由 数字、字母 组成")
|
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "用户账号由 数字、字母 组成")
|
||||||
@Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符")
|
@Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符")
|
||||||
@DiffLogField(name = "用户账号")
|
@DiffLogField(name = "用户账号")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue