首先准备工具类:需要加解密的接口预热
package org.springblade.gateway.provider;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName DecryptProvider
* @Description TODO 解密接口传参
* @Author YuanJiaLe
* @Date 2023/10/8 15:21
* @PackageName org.springblade.gateway.provider
* @Version 1.0.0
*/
public class DecryptProvider {
private static final List<String> Decrypt_URL = new ArrayList<>();
static {
Decrypt_URL.add("/data/post");
}
public static List<String> getUrlList() {
return Decrypt_URL;
}
}
package org.springblade.gateway.provider;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName EncryptProvider
* @Description TODO 加密接口返参
* @Author YuanJiaLe
* @Date 2023/10/8 15:21
* @PackageName org.springblade.gateway.provider
* @Version 1.0.0
*/
public class EncryptProvider {
private static final List<String> Encrypt_URL = new ArrayList<>();
/**
* 返回结果集 需要加密的接口
*/
static {
Encrypt_URL.add("/data/post");
}
public static List<String> getUrlList() {
return Encrypt_URL;
}
}
工具准备就绪
开始编写gateway的全局拦截器:
1. 接口返回响应体加密过滤器:
package org.springblade.gateway.filter;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Charsets;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springblade.common.utils.AESUtil;
import org.springblade.common.utils.RSAUtil;
import org.springblade.gateway.props.AuthProperties;
import org.springblade.gateway.provider.EncryptProvider;
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.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Objects;
/**
* @ClassName ResponseEncryptFilter
* @Description TODO 对接口返回参数进行加密
* @Author YuanJiaLe
* @Date 2023/10/8 14:48
* @PackageName org.springblade.gateway.filter
* @Version 1.0.0
*/
@Data
@Slf4j
@Component
public class ResponseEncryptFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpStatus statusCode = exchange.getResponse().getStatusCode();
//if (Objects.equals(statusCode, HttpStatus.BAD_REQUEST) || Objects.equals(statusCode, HttpStatus.TOO_MANY_REQUESTS)) {
// // 如果是特殊的请求,已处理响应内容,这里不再处理
// return chain.filter(exchange);
//}
// 如果是特殊的请求,已处理响应内容,这里不再处理
if (!Objects.equals(statusCode, HttpStatus.OK)) {
return chain.filter(exchange);
}
//校验路径
if (isSkip(request.getURI().getPath())) {
// 根据具体业务内容,修改响应体
return modifyResponseBody(exchange, chain);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
/**
* 修改响应体
*
* @param exchange
* @param chain
* @return
*/
private Mono<Void> modifyResponseBody(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator response = buildResponse(originalResponse, bufferFactory);
return chain.filter(exchange.mutate().response(response).build());
}
/**
* 构建响应
*
* @param originalResponse
* @param bufferFactory
* @return
*/
private ServerHttpResponseDecorator buildResponse(ServerHttpResponse originalResponse, DataBufferFactory bufferFactory) {
return new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
//只有在返回码为 200 的时候才对数据进行加密操作
if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
// 流转为字符串
String responseData = new String(content, Charsets.UTF_8);
//加密数据
JSONObject jsonObject = JSONObject.parseObject(responseData);
String code = String.valueOf(jsonObject.get("code"));
//返回响应码为200 才进行加密
if ("200".equals(code)) {
String data = String.valueOf(jsonObject.get("data"));
String key = AESUtil.getKey();
String encryptData = AESUtil.encrypt(data, key);
String encryptKey = RSAUtil.frontDecrypt(key);
//数据替换
jsonObject.put("data", new StringBuffer(encryptKey).append("&").append(encryptData).toString());
responseData = jsonObject.toJSONString();
}
byte[] uppedContent = responseData.getBytes(Charsets.UTF_8);
originalResponse.getHeaders().setContentLength(uppedContent.length);
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body);
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
};
}
/**
* 路径校验
*
* @param path
* @return
*/
private boolean isSkip(String path) {
return EncryptProvider.getUrlList().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)) || authProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path));
}
}
2. 接口入参body数据解密
package org.springblade.gateway.filter;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBufAllocator;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springblade.common.utils.AESUtil;
import org.springblade.common.utils.RSAUtil;
import org.springblade.gateway.props.AuthProperties;
import org.springblade.gateway.provider.DecryptProvider;
import org.springblade.gateway.provider.ResponseProvider;
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.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @ClassName DeCodeGlobalFilter
* @Description TODO 对接口请求数据body进行解密
* @Author YuanJiaLe
* @Date 2023/10/11 9:44
* @PackageName org.springblade.gateway.provider
* @Version 1.0.0
*/
@Data
@Slf4j
@Component
public class RequestDecryptFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpMethod method = request.getMethod();
String path = request.getURI().getPath();
//只针对post请求并且是指定的接口 进行解码
if (HttpMethod.POST.equals(method) && isSkip(path)) {
return handlePostMethod(exchange, request, chain);
} else if (HttpMethod.PUT.equals(method) && "/studentuser/update".equals(path)) {
return handlePostMethod(exchange, request, chain);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
/**
* 路径校验
*
* @param path
* @return
*/
private boolean isSkip(String path) {
return DecryptProvider.getUrlList().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)) || authProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path));
}
private Mono<Void> handlePostMethod(ServerWebExchange exchange, ServerHttpRequest request, GatewayFilterChain chain) {
MediaType contentType = request.getHeaders().getContentType();
if (contentType == null) {
return unAuth(exchange.getResponse(), "未识别的请求体类型");
}
StringBuffer paramBuffer = new StringBuffer();
Flux<DataBuffer> body = exchange.getRequest().getBody();
body.subscribe(buffer -> {
//解析body 数据
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
//拷贝数据
paramBuffer.append(charBuffer);
});
//获取到最后的body 中的数据
String param = paramBuffer.toString();
JSONObject JsonData = null;
if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
JsonData = JSONObject.parseObject(param);
}
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
JsonData = handleFormURLEncoded(param);
}
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
JsonData = JSON.parseObject(handleFormData(param, contentType.toString()));
}
if (JsonData == null) {
return unAuth(exchange.getResponse(), "不支持的请求体类型");
}
String res;
String[] split;
//解密请求体
split = String.valueOf(JsonData.get("data")).split("&");
res = AESUtil.decrypt(split[1], RSAUtil.decrypt(split[0]));
//如果是需要替换k-v类型得解密后的 循环替换解密后的数据【暂时用不到】
//for (String str : JsonData.keySet()) {
// //解密前的值
// String enCode = JsonData.getString(str);
// try {
// //解密请求体
// split = enCode.split("&");
// res = AESUtil.decrypt(split[1], RSAUtil.decrypt(split[0]));
// } catch (Exception e) {
// return unAuth(exchange.getResponse(), "解密失败");
// }
// //替换
// param = param.replace(enCode, res);
//}
//数据替换
String bodyStr = res;
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
int length = bodyStr.getBytes().length;
httpHeaders.putAll(super.getHeaders());
httpHeaders.remove(HttpHeaders.CONTENT_TYPE);
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.setContentLength(length);
httpHeaders.set(HttpHeaders.CONTENT_TYPE, contentType.toString());
// 设置CONTENT_TYPE
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
private JSONObject handleFormURLEncoded(String param) {
JSONObject jsonObject = new JSONObject();
String[] split = param.split("&");
for (String s : split) {
String[] split1 = s.split("=");
jsonObject.put(split1[0], split1[1]);
}
return jsonObject;
}
protected DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
private String handleFormData(String str, String contentType) {
String sep = "--" + contentType.replace("multipart/form-data;boundary=", "");
String[] strs = str.split("\r\n");
boolean bankRow = false;// 空行
boolean keyRow = true;// name=xxx行
boolean append = false;// 内容是否拼接换行符
Map<String, String> params = new LinkedHashMap<>();// 这里保证接收顺序,所以用linkedhashmap
String s = null, key = null;
StringBuffer sb = new StringBuffer();
for (int i = 1, len = strs.length - 1; i < len; i++) {
s = strs[i];
if (keyRow) {
key = s.replace("Content-Disposition: form-data; name=", "");
key = key.substring(1, key.length() - 1);
keyRow = false;
bankRow = true;
sb = new StringBuffer();
append = false;
continue;
}
if (sep.equals(s)) {
keyRow = true;
if (null != key) {
params.put(key, sb.toString());
}
append = false;
continue;
}
if (bankRow) {
bankRow = false;
append = false;
continue;
}
if (append) {
sb.append("\r\n");
}
sb.append(s);
append = true;
}
if (null != key) {
params.put(key, sb.toString());
}
return JSON.toJSONString(params);// 这里是alibaba的fastjson,需要引入依赖
}
private Mono<Void> unAuth(ServerHttpResponse resp, String msg) {
ObjectMapper objectMapper = new ObjectMapper();
//设置状态码
resp.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
//指定编码,否则在浏览器中会中文乱码
resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
String result = "";
try {
result = objectMapper.writeValueAsString(ResponseProvider.unAuth(msg));
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
}
DataBuffer buffer = resp.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
return resp.writeWith(Flux.just(buffer));
}
}
3.因为body只能读取一次,所以需要再写个
全局过滤器 解决body流重复获取
package org.springblade.gateway.filter;
import lombok.Data;
import org.springblade.gateway.props.AuthProperties;
import org.springblade.gateway.provider.DecryptProvider;
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.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
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.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @ClassName CacheRequestBodyFilter
* @Description TODO 全局过滤器 解决body流重复获取
* @Author YuanJiaLe
* @Date 2023/10/11 9:43
* @PackageName org.springblade.gateway.provider
* @Version 1.0.0
*/
@Data
@Component
public class CacheRequestBodyFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//如果不是指定接口 直接放行 不用处理
if (!isSkip(request.getURI().getPath())) {
return chain.filter(exchange);
}
if (request.getHeaders().getContentType() == null) {
return chain.filter(exchange);
}
HttpMethod method = request.getMethod();
if (method == null || method.matches("GET") || method.matches("DELETE")) {
return chain.filter(exchange);
}
//当body中没有缓存时,只会执行这一个拦截器, 原因是fileMap中的代码没有执行,所以需要在为空时构建一个空的缓存
DefaultDataBufferFactory defaultDataBufferFactory = new DefaultDataBufferFactory();
DefaultDataBuffer defaultDataBuffer = defaultDataBufferFactory.allocateBuffer(0);
//构建新数据流, 当body为空时,构建空流
Flux<DataBuffer> bodyDataBuffer = exchange.getRequest().getBody().defaultIfEmpty(defaultDataBuffer);
return DataBufferUtils.join(bodyDataBuffer)
.flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
//exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, cachedFlux);
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
@Override
public int getOrder() {
return 0;
}
/**
* 路径校验
*
* @param path
* @return
*/
private boolean isSkip(String path) {
return DecryptProvider.getUrlList().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)) || authProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path));
}
}