文章目录
- 前言
- 正文
- 一、项目简介
- 二、核心代码
- 2.1 自定义过滤器
- 2.2 网关配置
- 2.3 自定义配置类
- 2.4 加密组件接口
- 2.5 加密组件实现,AES算法
- 2.6 启动类,校验支持的算法配置
- 三、请求报文示例
- 四、测试结果
- 4.1 网关项目启动时
- 4.2 发生请求时
前言
本文环境使用比较新的 Java 17 和 SpringBoot 3.1.5,对应到Spring的版本是 6.0.13
使用到的三方插件有:
- lombok
- gson
- hutool
本文注重实现请求的解密和响应的加密,加解密使用的是 Hutool 中的工具类,加解密算法目前提供了AES的方式,其余方式也可兼容扩展。
完整代码仓库:https://gitee.com/fengsoshuai/springcloud-gateway-feng-demo
借用网关中的过滤器GlobalFilter
来实现这一功能。
本文只粘贴一些重点文件内容。
正文
一、项目简介
在聚合项目中,有两个核心模块,feng-server提供了 rest 接口,供网关使用。
feng-gateway 是核心实现的网关项目,实现了自定义过滤器,以及增加了一些基本配置功能。本文重心是网关项目。
二、核心代码
2.1 自定义过滤器
package org.feng.fenggateway.filters;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.feng.fenggateway.config.SecureProperties;
import org.feng.fenggateway.dto.ResponseDto;
import org.feng.fenggateway.secure.SecureComponent;
import org.feng.fenggateway.secure.SecureComponentFactory;
import org.feng.fenggateway.util.GsonUtil;
import org.reactivestreams.Publisher;
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.HttpHeaders;
import org.springframework.http.HttpMethod;
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.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Set;
/**
* 自定义密文过滤器
*
* @author feng
*/
@Slf4j
@Component
public class CustomCipherTextFilter implements GlobalFilter, Ordered {
@Resource
private SecureProperties secureProperties;
private SecureComponent secureComponent;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求体
ServerHttpRequest request = exchange.getRequest();
// 获取响应体
ServerHttpResponse response = exchange.getResponse();
// 请求头
HttpHeaders headers = request.getHeaders();
// 请求方法
HttpMethod method = request.getMethod();
// 满足条件,进行过滤
if (isNeedFilterMethod(method) && isNeedFilterContentType(headers.getContentType())) {
return DataBufferUtils.join(request.getBody())
.flatMap(dataBuffer -> {
try {
// 获取请求参数
String originalRequestBody = getOriginalRequestBody(dataBuffer);
// 解密请求参数
String decryptRequestBody = decryptRequest(originalRequestBody);
// 装饰新的请求体
ServerHttpRequestDecorator requestDecorator = serverHttpRequestDecorator(request, decryptRequestBody);
// 装饰新的响应体
ServerHttpResponseDecorator responseDecorator = serverHttpResponseDecorator(response);
// 使用新的请求和响应转发
ServerWebExchange serverWebExchange = exchange.mutate().request(requestDecorator).response(responseDecorator).build();
// 放行拦截
return chain.filter(serverWebExchange);
} catch (Exception e) {
log.error("密文过滤器加解密错误", e);
return Mono.empty();
} finally {
DataBufferUtils.release(dataBuffer);
}
});
}
return chain.filter(exchange);
}
private String decryptRequest(String originalRequestBody) {
if (!secureProperties.enableDecryptRequestParam()) {
log.info("请求参数解密,跳过");
return originalRequestBody;
}
log.info("请求参数解密,原文:{}", originalRequestBody);
String decrypted = getSecureComponent().decrypt(originalRequestBody);
log.info("请求参数解密,明文:{}", decrypted);
return decrypted;
}
private String encryptResponse(String originalResponseBody) {
if (!secureProperties.enableEncryptResponseParam()) {
log.info("响应结果加密,跳过");
return originalResponseBody;
}
ResponseDto responseDto = GsonUtil.fromJson(originalResponseBody, ResponseDto.class);
// 只对data字段进行加密处理
Object data = responseDto.getData();
if (Objects.nonNull(data)) {
responseDto.setData(getSecureComponent().encrypt(data.toString()));
}
log.info("响应结果加密,原文:{}", originalResponseBody);
String result = GsonUtil.toJson(responseDto);
log.info("响应结果加密,密文:{}", result);
return result;
}
/**
* 获取原始的请求参数
*
* @param dataBuffer 数据缓冲
* @return 原始的请求参数
*/
private String getOriginalRequestBody(DataBuffer dataBuffer) {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
return new String(bytes, StandardCharsets.UTF_8);
}
private boolean isNeedFilterMethod(HttpMethod method) {
return NEED_FILTER_METHOD_SET.contains(method);
}
private boolean isNeedFilterContentType(MediaType mediaType) {
return NEED_FILTER_MEDIA_TYPE_SET.contains(mediaType) || "json".equals(mediaType.getSubtype());
}
private ServerHttpRequestDecorator serverHttpRequestDecorator(ServerHttpRequest originalRequest, String decryptRequestBody) {
return new ServerHttpRequestDecorator(originalRequest) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
byte[] bytes = decryptRequestBody.getBytes(StandardCharsets.UTF_8);
return Flux.just(new DefaultDataBufferFactory().wrap(bytes));
}
};
}
private ServerHttpResponseDecorator serverHttpResponseDecorator(ServerHttpResponse originalResponse) {
DataBufferFactory dataBufferFactory = originalResponse.bufferFactory();
return new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux<? extends DataBuffer> fluxBody) {
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] byteArray = new byte[join.readableByteCount()];
join.read(byteArray);
DataBufferUtils.release(join);
String originalResponseBody = new String(byteArray, StandardCharsets.UTF_8);
//加密
byte[] encryptedByteArray = encryptResponse(originalResponseBody).getBytes(StandardCharsets.UTF_8);
originalResponse.getHeaders().setContentLength(encryptedByteArray.length);
originalResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
return dataBufferFactory.wrap(encryptedByteArray);
}));
}
return super.writeWith(body);
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.putAll(originalResponse.getHeaders());
return headers;
}
};
}
private static final Set<HttpMethod> NEED_FILTER_METHOD_SET = Set.of(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT);
private static final Set<MediaType> NEED_FILTER_MEDIA_TYPE_SET = Set.of(MediaType.APPLICATION_JSON);
@Override
public int getOrder() {
return -1;
}
public SecureComponent getSecureComponent() {
if (Objects.isNull(secureComponent)) {
secureComponent = SecureComponentFactory.get(secureProperties.getAlgorithm());
}
return secureComponent;
}
}
2.2 网关配置
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
gateway:
routes: # 网关路由配置
- id: feng-server1 # 路由id,自定义,只要唯一即可
uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/server/list/server1/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: feng-server2
uri: http://127.0.0.1:8082
predicates:
- Path=/server/list/server2/**
# 自定义配置
feng:
gateway:
secure:
request-switch:
enable: false
response-switch:
enable: true
algorithm: aes
2.3 自定义配置类
package org.feng.fenggateway.config;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.feng.fenggateway.secure.SecureComponentFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Objects;
/**
* 加解密属性配置
*
* @author feng
*/
@Slf4j
@Data
@ConfigurationProperties(prefix = SecureProperties.SECURE_PROPERTIES_PREFIX)
public class SecureProperties {
public static final String SECURE_PROPERTIES_PREFIX = "feng.gateway.secure";
/**
* 算法
*/
private SymmetricAlgorithm algorithm;
/**
* 请求开关
*/
private SecureSwitch requestSwitch;
/**
* 响应开关
*/
private SecureSwitch responseSwitch;
public void checkSupportedAlgorithm() {
log.info("校验是否支持算法:{}", algorithm);
if (Objects.isNull(algorithm)) {
return;
}
boolean supportedAlgorithm = SecureComponentFactory.isSupportedAlgorithm(algorithm);
if (!supportedAlgorithm) {
throw new UnsupportedOperationException("不支持的算法");
}
log.info("校验是否支持算法:校验通过");
}
/**
* 是否启用解密请求参数
*
* @return 默认为否,其他情况看配置
*/
public boolean enableDecryptRequestParam() {
if (Objects.isNull(requestSwitch)) {
return false;
}
return requestSwitch.getEnable();
}
/**
* 是否启用加密响应参数
*
* @return 默认为否,其他情况看配置
*/
public boolean enableEncryptResponseParam() {
if (Objects.isNull(responseSwitch)) {
return false;
}
return responseSwitch.getEnable();
}
}
2.4 加密组件接口
这个可以用来扩展支持其他加密算法,目前实现类只有AES。
package org.feng.fenggateway.secure;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import jakarta.annotation.PostConstruct;
/**
* 加解密组件
*
* @author feng
*/
public interface SecureComponent {
/**
* 加密
*
* @param originalText 原文
* @return 密文
*/
String encrypt(String originalText);
/**
* 解密
*
* @param encryptedText 密文
* @return 解密后的明文
*/
String decrypt(String encryptedText);
/**
* 获取加解密算法类型
*
* @return 加解密算法类型
*/
SymmetricAlgorithm getAlgorithmType();
@PostConstruct
default void registerToFactory() {
SecureComponentFactory.registerBean(this);
}
}
2.5 加密组件实现,AES算法
package org.feng.fenggateway.secure;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* AES加解密组件
*
* @author feng
*/
@Component
public class SecureAESComponent implements SecureComponent {
/**
* 生成密钥,16、24、32位都行
*/
private final static byte[] SECURE_KEY = "r4oz0f3kfk5tgyui".getBytes(StandardCharsets.UTF_8);
/**
* 偏移量,必须16位
*/
private final static String IV = "r21g95kdsd423gy6";
private final static AES AES_INSTANCE = new AES(Mode.CTS, Padding.PKCS5Padding, SECURE_KEY, IV.getBytes(StandardCharsets.UTF_8));
@Override
public String encrypt(String originalText) {
return AES_INSTANCE.encryptHex(originalText);
}
@Override
public String decrypt(String encryptedText) {
return AES_INSTANCE.decryptStr(encryptedText);
}
@Override
public SymmetricAlgorithm getAlgorithmType() {
return SymmetricAlgorithm.AES;
}
}
2.6 启动类,校验支持的算法配置
package org.feng.fenggateway;
import jakarta.annotation.Resource;
import org.feng.fenggateway.config.SecureProperties;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@ConfigurationPropertiesScan
@SpringBootApplication
public class FengGatewayApplication implements CommandLineRunner {
@Resource
private SecureProperties secureProperties;
public static void main(String[] args) {
SpringApplication.run(FengGatewayApplication.class, args);
}
@Override
public void run(String... args) {
secureProperties.checkSupportedAlgorithm();
}
}
三、请求报文示例
POST http://localhost:10010/server/list/server2/user?authorization=feng
Content-Type: application/json;charset=UTF-8
{
"username": "fbb"
}
四、测试结果
4.1 网关项目启动时
校验结果正常:
4.2 发生请求时
可以看到data字段已经加密响应了。
请求和响应结果: