背景

为了增强产品安全性,计划对应用网关进行改造,主要是出入参经过网关时需要进行加解密操作,保证请求数据在网络传输过程中不会泄露或篡改。

考虑到密钥的安全性,每个用户登录都会签发独立的密钥对。同时摒弃了对称加密算法,使用国密非对称的SM2算法进行参数加解密。

网关加解密全流程时序图

难点

先说下开发过程中遇到的一些困难,后面再看代码就知道为什么这么写。

1、网上有价值可供参考的代码不多,这也是为什么要写这边博客的原因,网上现有代码大部分都是互相照搬的,实测过程会发现有很多问题,比如ServerHttpRequestDecorator要重复new很多遍。

2、由于Gateway是基于WebFlux的非阻塞线程模型开发的,在读取RequestBody时可能会出现读取不完整的问题,而且是偶发现象,同样的问题在重写ResponseBody时也会遇到。

3、性能问题,SM2算法是基于bcprov-jdk15on开源库,加解密过程需要对密钥对进行缓存,如果通过16进制字符串进行序列化耗时过长,会造成网关性能瓶颈。

SM2

先说SM2加解密算法这块。

用的是全球最大同性交友网站开源的一个项目,对SM2加解密操作进行了一些封装,项目地址:https://github.com/ZZMarquis/gmhelper

因为每个用户登录时都会签发密钥对,所以每次加解密需要获取用户对应的密钥对后再进行参数加解密操作。

为了避免每次通过密钥对字符串创建密钥对象增加代码执行耗时,用户密钥对使用protostuff序列化为字符串后en缓存在Redis中,需要使用的时候直接从Redis中读取出来反序列化为密钥对象即可,这一步大大提升了代码性能。

该部分代码如下:

pom.xml

<dependency>
<groupId>org.zz</groupId>
<artifactId>gmhelper</artifactId>
<version>1.0.0</version>
</dependency> <dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.8.0</version>
</dependency> <dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.8.0</version>
</dependency>

密钥对PO对象

public class SM2Key implements Serializable {

    private static final long serialVersionUID = 8273826788748051389L;
/**
* 后端加密公钥,对应私钥由前端持有(webPrivateKey)
*/

private ECPublicKeyParameters serverPublicKey; /**
* 后端解密私钥,对应公钥由前端持有(webPublicKey)
*/
private ECPrivateKeyParameters serverPrivateKey; /**
* 前端加密公钥,对应私钥由后端持有(serverPrivateKey)
*/
private String webPublicKey; /**
* 前端解密私钥,对应公钥由后端持有(serverPublicKey)
*/
private String webPrivateKey; public static SM2Key build() {
return new SM2Key();
} public static SM2Key build(String protostuffHex) {
final byte[] protostuffBytes = ByteUtils.fromHexString(protostuffHex);
Schema schema = RuntimeSchema.getSchema(SM2Key.class);
SM2Key key = RuntimeSchema.getSchema(SM2Key.class).newMessage();
GraphIOUtil.mergeFrom(protostuffBytes, key, schema);
return key;
} public ECPublicKeyParameters getServerPublicKey() {
return serverPublicKey;
} public void setServerPublicKey(ECPublicKeyParameters serverPublicKey) {
this.serverPublicKey = serverPublicKey;
} public ECPrivateKeyParameters getServerPrivateKey() {
return serverPrivateKey;
} public void setServerPrivateKey(ECPrivateKeyParameters serverPrivateKey) {
this.serverPrivateKey = serverPrivateKey;
} public String getWebPublicKey() {
return webPublicKey;
} public void setWebPublicKey(String webPublicKey) {
this.webPublicKey = webPublicKey;
} public String getWebPrivateKey() {
return webPrivateKey;
} public void setWebPrivateKey(String webPrivateKey) {
this.webPrivateKey = webPrivateKey;
} /**
* 对象序列化
* @return
*/
public String toProtostuffString() {
LinkedBuffer buffer = LinkedBuffer.allocate();
try {
Schema<SM2Key> schema = RuntimeSchema.getSchema(SM2Key.class);
final byte[] protostuff = GraphIOUtil.toByteArray(this, schema, buffer);
return ByteUtils.toHexString(protostuff);
} finally {
buffer.clear();
}
} }

密钥对签发工具类

public class SM2KeyUtil {

    /**
* 生成前后端加解密密钥对
* @return
*/
public static SM2Key generate() { // 构建前后端密钥对
SM2Key key = SM2Key.build(); AsymmetricCipherKeyPair keyPair;
ECPrivateKeyParameters privateKey;
ECPublicKeyParameters publicKey; keyPair = SM2Util.generateKeyPairParameter();
privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
publicKey = (ECPublicKeyParameters) keyPair.getPublic();
// 后端加密所需公钥
key.setServerPublicKey(publicKey);
// 前端解密所需私钥
key.setWebPrivateKey(ByteUtils.toHexString(privateKey.getD().toByteArray())); keyPair = SM2Util.generateKeyPairParameter();
privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
publicKey = (ECPublicKeyParameters) keyPair.getPublic();
// 后端解密所需私钥
key.setServerPrivateKey(privateKey);
// 前端加密所需公钥
key.setWebPublicKey(ByteUtils.toHexString(publicKey.getQ().getEncoded(false))); return key; } }

通过SM2Key.toProtostuffString()方法获得序列化字符串并写入Redis中

全局拦截器

全局拦截器配置类

/**
* 网关加解密配置类
* @author changxy
*/
@Configuration
@ConditionalOnProperty(value = "secret.enabled", havingValue = "true", matchIfMissing = true)
public class SecretConfiguration { private static final Logger log = LoggerFactory.getLogger(SecretConfiguration.class); /**
* 免加密接口配置
*/
public static final String EXCLUDE_PATH_CONFIG_KEY = "#{'${secret.excluded.paths}'.split(',')}"; /**
* 注册入参解密全局拦截器
* @param secretFormatterAdapter 加解密格式化适配器
* @param decryptRequestBodyFilterFactory 入参解密拦截器工厂,主要为了读取Body
* @param requestBodyDecryptRewriter RequestBody参数解密RewriteFunction
* @return
*/
@Bean
public DecryptParameterFilter decryptParameterFilter(
@Autowired SecretFormatterAdapter secretFormatterAdapter,
@Autowired ModifyRequestBodyGatewayFilterFactory decryptRequestBodyFilterFactory,
@Autowired RequestBodyDecryptRewriter requestBodyDecryptRewriter
) {
log.info("初始化入参解密全局拦截器");
return new DecryptParameterFilter(secretFormatterAdapter, decryptRequestBodyFilterFactory, requestBodyDecryptRewriter);
} /**
* 注册出参加密拦截器
* !!!!免加密配置项中的接口出参不进行加密处理!!!!
* @param secretFormatterAdapter 加解密格式化适配器
* @param encryptFilterFactory 出参加密拦截器工厂,对Content-Type为JSON的响应内容加密处理
* @param jsonEncryptRewriter ResponseBody参数加密RewriteFunction
* @param excludedPaths 免加密接口配置
* @return
*/
@Bean
public EncryptResponseFilter encryptResponseFilter(
@Autowired SecretFormatterAdapter secretFormatterAdapter,
@Autowired ModifyResponseBodyGatewayFilterFactory encryptFilterFactory,
@Autowired ResponseJSONEncryptRewriter jsonEncryptRewriter,
@Value(EXCLUDE_PATH_CONFIG_KEY) List<String> excludedPaths
) {
log.info("初始化出参加密全局拦截器");
return new EncryptResponseFilter(secretFormatterAdapter, encryptFilterFactory, jsonEncryptRewriter, excludedPaths);
} /**
* 入参解密拦截器工厂,主要为了读取Body
* @param secretFormatterAdapter 加解密格式化适配器
* @return
*/
@Bean
RequestBodyDecryptRewriter requestBodyDecryptRewrite(@Autowired SecretFormatterAdapter secretFormatterAdapter) {
return new RequestBodyDecryptRewriter(secretFormatterAdapter);
} /**
* 出参加密拦截器工厂,对Content-Type为JSON的响应内容加密处理
* @param secretFormatterAdapter 加解密格式化适配器
* @return
*/
@Bean
ResponseJSONEncryptRewriter responseJSONEncryptRewriter(@Autowired SecretFormatterAdapter secretFormatterAdapter) {
return new ResponseJSONEncryptRewriter(secretFormatterAdapter);
} }

全局入参解密拦截器,为了安全起见,保留关键代码,部分常量被移除。

DecryptedServerHttpRequestDecorator通过重写URI实现querystring部分参数的解密处理,同时在路由转发前增加secret请求头。

RequestBodyDecryptRewriter是RewriteFunction的实现类,主要读取RequestBody内容进行解密、重写操作,这里使用RewriteFunction可以获取完整的Body内容。

public class DecryptParameterFilter implements GlobalFilter, Ordered {

    private final static Logger log = LoggerFactory.getLogger(DecryptParameterFilter.class);

    protected static final List<MediaType> ENCRYPT_MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_JSON,
MediaType.APPLICATION_JSON_UTF8,
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.valueOf("application/x-www-form-urlencoded;charset=UTF-8")); /**
* 加解密序列化适配器
*/
private final SecretFormatterAdapter secretFormatterAdapter; private final ModifyRequestBodyGatewayFilterFactory decryptRequestBodyFilterFactory; private final RequestBodyDecryptRewriter requestBodyDecryptRewriter; public DecryptParameterFilter(
SecretFormatterAdapter secretFormatterAdapter,
ModifyRequestBodyGatewayFilterFactory decryptRequestBodyFilterFactory,
RequestBodyDecryptRewriter requestBodyDecryptRewriter) {
this.secretFormatterAdapter = secretFormatterAdapter;
this.decryptRequestBodyFilterFactory = decryptRequestBodyFilterFactory;
this.requestBodyDecryptRewriter = requestBodyDecryptRewriter;
} @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 读取token
String token = exchange.getRequest().getHeaders().getFirst(SystemSsoLoginStore.SSO_TOKEN); // 通过token在redis读取用户信息
SystemSsoUser user = SystemSsoLoginHelper.loginCheck(token); // 设置用户信息上下文
exchange.getAttributes().put(SystemSsoTokenFilter.GATEWAY_SSO_USER_ATTR, user);
// 设置用户上下文对象,用户加解密读取公私钥
exchange.getAttributes().put(GATEWAY_SSO_USER_KEYS_ATTR, SM2Key.build(user.getSecretKey())); // 请求类型不需要参数解密,只使用Request封装类不用重写Body
if (!ENCRYPT_MEDIA_TYPES.contains(exchange.getRequest().getHeaders().getContentType())) {
// 使用封装类为了在请求头中增加secret标记
return buildRequestDecorator(exchange, chain);
} return decryptRequestBodyFilterFactory
.apply(new ModifyRequestBodyGatewayFilterFactory.Config().setRewriteFunction(String.class, String.class, requestBodyDecryptRewriter))
.filter(new DecryptedServerWebExchangeDecorator(exchange, secretFormatterAdapter), chain);
} /**
* 构建Request封装类
* @param exchange
* @param chain
* @return
*/
protected Mono<Void> buildRequestDecorator(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange.mutate().request(new DecryptedServerHttpRequestDecorator(exchange, exchange.getRequest())).build());
} @Override
public int getOrder() {
// 需要对exchange和request对象进行封装,所以优先级放到最高
// 优先级过低可能会造成拦截器不生效
return Ordered.HIGHEST_PRECEDENCE;
} /**
* ServerHttpRequest解密封装类
* 1、处理queryString参数解密
* 2、处理body参数解密
* @author changxy
*/
static class DecryptedServerHttpRequestDecorator extends ServerHttpRequestDecorator { private ServerWebExchange originExchange; private ServerHttpRequest originRequest; public DecryptedServerHttpRequestDecorator(ServerWebExchange originExchange, ServerHttpRequest originRequest, SecretFormatterAdapter secretFormatterAdapter) {
super(originRequest);
this.originExchange = originExchange;
this.originRequest = originRequest;
this.secretFormatterAdapter = secretFormatterAdapter;
} @Override
public URI getURI() { // 获取原始请求链接
URI uri = super.getURI();
// 获取原始QueryString请求参数
MultiValueMap<String, String> originQueryParams = originRequest.getQueryParams(); // 处理QueryString请求参数解密
if (Objects.nonNull(originQueryParams) && originQueryParams.containsKey(ENCRYPT_QUERY_STRING_KEY)) {
// 获取密文
List<String> encrypted = originQueryParams.get(ENCRYPT_QUERY_STRING_KEY);
// 非空校验
if (Objects.nonNull(encrypted) && !encrypted.isEmpty()) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri); // 清空原有queryString
uriComponentsBuilder.query(null); for (String encrypt : encrypted) {
// 解密并放入queryString中
uriComponentsBuilder.query(Sm2Factory.getInstance().decrypt(originExchange, encrypt));
} // build(true) 不会再次进行URL编码
uri = uriComponentsBuilder.build(true).toUri();
return uri;
}
}
return super.getURI();
} @Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.putAll(super.getHeaders());
// 请求微服务应用时添加加解密请求头,门户根据请求头签发证书,前端进行入参加密
headers.put("secret", Collections.singletonList(Boolean.TRUE.toString()));
return headers;
} } /**
* ServerWebExchange包装类,这里主要为了包装ServerHttpRequest
*/
static class DecryptedServerWebExchangeDecorator extends ServerWebExchangeDecorator { private final ServerHttpRequestDecorator requestDecorator; protected DecryptedServerWebExchangeDecorator(ServerWebExchange delegate, SecretFormatterAdapter secretFormatterAdapter) {
super(delegate);
this.requestDecorator = new DecryptedServerHttpRequestDecorator(delegate, delegate.getRequest(), secretFormatterAdapter);
} @Override
public ServerHttpRequest getRequest() {
return requestDecorator;
}
} }

RequestBodyDecryptRewriter

/**
* RequestBody参数重写类
* @author changxy
*/
public class RequestBodyDecryptRewriter implements RewriteFunction<String, String> { /**
* 加解密序列化适配器
*/
private final SecretFormatterAdapter secretFormatterAdapter; public RequestBodyDecryptRewriter(SecretFormatterAdapter secretFormatterAdapter) {
this.secretFormatterAdapter = secretFormatterAdapter;
} @Override
public Publisher<String> apply(ServerWebExchange exchange, String body) {
return Mono.just(decryptBody(exchange, body));
} protected String decryptBody(ServerWebExchange exchange, String body) {
if (StringUtils.hasText(body)) {
return secretFormatterAdapter.format(exchange, SecretFormatter.SecretFormatterType.DECRYPT, body);
} return body;
}
}

全局加密拦截器

实现原理和全局解密拦截器类似,这里不再赘述。

EncryptResponseFilter

public class EncryptResponseFilter implements GlobalFilter, Ordered {

    protected static final List<MediaType> MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8);

    /**
* 加解密序列化适配器
*/
private final SecretFormatterAdapter secretFormatterAdapter; private final ModifyResponseBodyGatewayFilterFactory encryptFilterFactory; private final ResponseJSONEncryptRewriter jsonEncryptRewriter; private final List<String> excludedPaths; public EncryptResponseFilter(
SecretFormatterAdapter secretFormatterAdapter,
ModifyResponseBodyGatewayFilterFactory encryptFilterFactory,
ResponseJSONEncryptRewriter jsonEncryptRewriter,
List<String> excludedPaths
) {
this.secretFormatterAdapter = secretFormatterAdapter;
this.encryptFilterFactory = encryptFilterFactory;
this.jsonEncryptRewriter = jsonEncryptRewriter;
this.excludedPaths = excludedPaths;
} @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 不处理免加密接口
if (PathMatcherFactoryInstance.match(excludedPaths, exchange.getRequest().getURI().getPath())) {
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().response(new EncryptServerHttpResponseDecorator(exchange, encryptFilterFactory, jsonEncryptRewriter, chain)).build());
} @Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
} /**
* ServerHttpResponse封装类
*/
static class EncryptServerHttpResponseDecorator extends ServerHttpResponseDecorator { private final ServerHttpResponse serverHttpResponse; private final ModifyResponseBodyGatewayFilterFactory encryptFilterFactory; private final ResponseJSONEncryptRewriter jsonEncryptRewriter; private final ServerWebExchange serverWebExchange; private final GatewayFilterChain chain; public EncryptServerHttpResponseDecorator(
ServerWebExchange serverWebExchange,
ModifyResponseBodyGatewayFilterFactory encryptFilterFactory,
ResponseJSONEncryptRewriter jsonEncryptRewriter,
GatewayFilterChain chain
) {
super(serverWebExchange.getResponse());
this.serverHttpResponse = serverWebExchange.getResponse();
this.serverWebExchange = serverWebExchange;
this.encryptFilterFactory = encryptFilterFactory;
this.jsonEncryptRewriter = jsonEncryptRewriter;
this.chain = chain;
} @Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
// 这里只处理返回JSON格式的响应信息
if (MEDIA_TYPES.contains(serverHttpResponse.getHeaders().getContentType()) && body instanceof Flux) {
// 通过RewriteFunction重写ResponseBody
return encryptFilterFactory.apply(new ModifyResponseBodyGatewayFilterFactory.Config().setRewriteFunction(String.class, String.class, jsonEncryptRewriter)).filter(serverWebExchange, chain);
} else {
return super.writeWith(body);
}
} @Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(publisher -> publisher));
}
} }

ResponseJSONEncryptRewriter

public class ResponseJSONEncryptRewriter implements RewriteFunction<String, String> {

    /**
* 加解密序列化适配器
*/
private final SecretFormatterAdapter secretFormatterAdapter; public ResponseJSONEncryptRewriter(SecretFormatterAdapter secretFormatterAdapter) {
this.secretFormatterAdapter = secretFormatterAdapter;
} @Override
public Publisher<String> apply(ServerWebExchange exchange, String json) {
return Mono.just(encrypt(exchange, json));
} public String encrypt(ServerWebExchange exchange, String json) {
if (StringUtils.hasText(json)) {
return secretFormatterAdapter.format(exchange, SecretFormatter.SecretFormatterType.ENCRYPT, json);
}
return json;
} }

代码中涉及到的SecretFormatterAdapter是针对不同场景实现加解密序列化适配器类,大家可以根据需求自行实现。

大家对上述代码有疑问或建议的,欢迎交流指正。

如何优雅而不损失性能的实现SpringCloud Gateway网关参数加解密方案的更多相关文章

  1. SpringCloud GateWay网关(入门)

    1.介绍 强烈推荐,看官网文档 Spring Cloud Gateway ①简介 Cloud全家桶里有个重要组件:网关 SpringCloud Gateway基于WebFlux框架 WebFlux底层 ...

  2. SpringCloud gateway (史上最全)

    疯狂创客圈 Java 分布式聊天室[ 亿级流量]实战系列之 -25[ 博客园 总入口 ] 前言 ### 前言 疯狂创客圈(笔者尼恩创建的高并发研习社群)Springcloud 高并发系列文章,将为大家 ...

  3. SpringCloud gateway 3

    参考博客:https://www.cnblogs.com/crazymakercircle/p/11704077.html 1.1 SpringCloud Gateway 简介 SpringCloud ...

  4. SpringCloud Gateway微服务网关实战与源码分析-上

    概述 定义 Spring Cloud Gateway 官网地址 https://spring.io/projects/spring-cloud-gateway/ 最新版本3.1.3 Spring Cl ...

  5. SpringCloud(7)---网关概念、Zuul项目搭建

    SpringCloud(7)---网关概念.Zuul项目搭建 一.网关概念 1.什么是路由网关 网关是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能 提供路由请求.鉴权.监控. ...

  6. MySQL性能优化方法一:缓存参数优化

    原文链接:http://isky000.com/database/mysql-perfornamce-tuning-cache-parameter 数据库属于 IO 密集型的应用程序,其主要职责就是数 ...

  7. SpringCloud之网关 Gateway(五)

    前面我们在聊服务网关Zuul的时候提到了Gateway,那么Zuul和Gateway都是服务网关,这两个有什么区别呢? 1. Zuul和Gateway的恩怨情仇 1.1 背景 Zuul是Netflix ...

  8. 微服务实战系列(七)-网关springcloud gateway

    1. 场景描述 springcloud刚推出的时候用的是netflix全家桶,路由用的zuul,但是据说zull1.0在大数据量访问的时候存在较大性能问题,2.0就没集成到springcloud中了, ...

  9. SpringCloud Gateway快速入门

    SpringCloud Gateway cloud笔记第一部分 cloud笔记第二部分Hystrix 文章目录 SpringCloud Gateway Zull的工作模式与Gateway的对比 Rou ...

  10. 万字长文:SpringCloud gateway入门学习&实践

    官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/# ...

随机推荐

  1. IDA常用的插件

    IDA常用的插件 FindCrypto https://github.com/polymorf/findcrypt-yara 算法识别 缺点:对于魔改的地方难以识别,比如对aes的s盒进行加密,运行时 ...

  2. (洛谷P4213)杜教筛

    https://www.cnblogs.com/Mychael/p/8744633.html #pragma GCC optimize(3, "Ofast", "inli ...

  3. 谈谈JSF业务线程池的大小配置

    1.简介 JSF业务线程池使用JDK的线程池技术,缺省情况下采用Cached模式(核心线程数20,最大线程数200).此外,还提供了Fixed固定线程大小的模式,两种模式均可设置请求队列大小. 本文旨 ...

  4. Azure Data Factory(七)数据集验证之用户托管凭证

    一,引言 上一篇文章中,我们讲解了 Azure Data Factory 在设置数据集类型为  Dataverse 的时候,如何连接测试.今天我们继续讲解认证方式这一块内容,打开 Link Servi ...

  5. 【.NET8】访问私有成员新姿势UnsafeAccessor(上)

    前言 前几天在.NET性能优化群里面,有群友聊到了.NET8新增的一个特性,这个类叫UnsafeAccessor,有很多群友都不知道这个特性是干嘛的,所以我就想写一篇文章来带大家了解一下这个特性. 其 ...

  6. Solution Set -「ABC 192」

    「ABC 113A」Star Link. 略. #include<cstdio> int x; int main() { scanf("%d",&x); for ...

  7. No module named virtualenvwrapper 虚拟环境报错

    No module named virtualenvwrapper 虚拟环境报错 安装虚拟环境命令 sudo pip install virtualenv sudo pip install virtu ...

  8. Django框架项目之支付功能——支付宝支付

    文章目录 支付宝支付 入门 支付流程 aliapy二次封装包 GitHub开源框架 依赖 结构 alipay_public_key.pem app_private_key.pem setting.py ...

  9. Python操作Word水印:添加文字或图片水印

    在Word文档中,可以添加半透明的图形或文字作为水印,以保护文档的原创性,防止未经授权的复制或使用.除了提供安全功能外,水印还可以展示文档创作者的信息.附加的文档信息,或者仅用于文档的装饰.本文将介绍 ...

  10. 『STAOI』G - Round 2 半个游记

    很刺激. 2023.3.2 23:17 第一次过审. 2023.3.5 00:02 第一次打回. 原因是背锅人的链接又双叒叕挂错了((( 2023.3.6 21:20 第二次过审. 2023.3.8 ...