前後端request body response body加解密,安全整改

pull/166/head
让无线电飞BG8GLR 2025-01-10 22:11:11 +08:00
parent fb355064d8
commit 68d7572391
6 changed files with 289 additions and 3 deletions

View File

@ -1,9 +1,12 @@
package cn.iocoder.yudao.gateway;
import cn.iocoder.yudao.gateway.filter.front.ParamterSecretProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(ParamterSecretProperties.class)
public class GatewayServerApplication {
public static void main(String[] args) {

View File

@ -11,7 +11,7 @@ import java.io.Serializable;
* @author feng
*/
@Data
public class ResponseDto implements Serializable {
public class BodyDto implements Serializable {
@Serial
private static final long serialVersionUID = -6354714160436354659L;
@ -20,4 +20,8 @@ public class ResponseDto implements Serializable {
private String msg;
private Object data;
private String requestBody;
private String responseBody;
}

View File

@ -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;
}
/**
* headersbody
*
* @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();
}
};
}
}

View File

@ -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; // 加密秘钥
}

View File

@ -98,7 +98,7 @@ public class AccessLogFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
return Ordered.HIGHEST_PRECEDENCE+99;
}
@Override

View File

@ -23,7 +23,7 @@ public class UserSaveReqVO {
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
@NotBlank(message = "用户账号不能为空")
@Pattern(regexp = "^[a-zA-Z0-9]$", message = "用户账号由 数字、字母 组成")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "用户账号由 数字、字母 组成")
@Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符")
@DiffLogField(name = "用户账号")
private String username;