前言:近期在项目中需要对指定请求整个信道进行加解密(防篡改,反拦截),实现整个信道通讯安全,由于不确定后期那些接口可能需要类似处理,最后决定放在网关实现该功能(虽然稍微占用网关的性能,但是能够全局统一处理十分方便,性能方面可以增加配置来解决)。此过程中也踩了些坑,在此记录下来,以便后来人参考;
首先我们采用的是GlobalFilter,GlobalFilter是应用于所有路由的特殊过滤器。
GlobalFilter接口的实现类如下图所示:
当请求与路由匹配时,Web 处理程序会将所有的GlobalFilter和特定的GatewayFilter添加到过滤器链中。这个组合过滤器链是按org.springframework.core.Ordered接口排序的,也通过实现getOrder()方法来设置。
整体流程是,前端按照我们约定的加密方式将数据进行加密传给后端,网关拦截后对其解密并请求转发,消费方返回结果后,网关统一处理请求,并对其加密返回前端
基于GlobalFilter 接口包装请求request和响应response,先列出关键代码,完整代码见文末
1、首先我们将我们接收到的请求参数进行实例化,并根据自己的加解密规则进行解密
byte[] bytes = new byte[dataBuffer.readableByteCount()];
String requestBody = new String(bytes, Charset.forName("UTF-8"));
//实例化请求体
SecurityRequest securityRequest = JSONObject.parseObject(requestBody, SecurityRequest.class);
if (securityRequest == null) {
chain.filter(exchange);
}
//解密请求参数
String decryptedRequestStr = decryptParams(securityRequest);
2、将解密后的请求体写入新的request
ServerHttpRequestDecorator newRequest = new ServerHttpRequestDecorator(request) {
@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 = decryptedRequestStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = new DefaultDataBufferFactory().wrap(bytes);
return Flux.just(buffer);
}
};
3、ServerHttpResponseDecorator处理请求返回,统一进行加密处理(这里的AES加密只是示例,可根据自己封装的加密方法进行加密,重在关注怎末读取返回,和处理后写回)
ServerHttpResponse decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] byteArray = new byte[join.readableByteCount()];
join.read(byteArray);
String originalResponseBody = new String(byteArray, Charset.forName("UTF-8"));
//加密
byte[] encryptedByteArray = encryptResponse(originalResponseBody, SecurityRequest.getAesKey()).getBytes(StandardCharsets.UTF_8);
DataBuffer wrap = dataBufferFactory.wrap(encryptedByteArray);
DataBufferUtils.release(join);
return wrap;
}));
}
return super.writeWith(body);
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
};
4、对请求进行转发
ServerWebExchange newExchange = exchange.mutate().request(newRequest).response(decoratedResponse).build();
完整代码如下:
package com.xxx.gateway.filter.security;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.xxx.gateway.config.SecurityProperties;
import com.xxx.gateway.enums.BaseResponseEnum;
import com.xxx.gateway.security.ImpSatSecurity;
import com.xxx.gateway.util.AESCBCUtils;
import com.xxx.gateway.util.BizException;
import com.xxx.gateway.util.UrlPathUtils;
import com.xxx.gateway.vo.SecurityRequest;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Set;
@Component
@Slf4j
@Data
public class SecurityFilter implements GlobalFilter, Ordered {
// 超时时间5分钟
public static final long OVER_TIMES = 300000;
protected Logger logger = LoggerFactory.getLogger(this.getClass());
//开关配置
@Autowired
private SecurityProperties securityProperties;
//加密工厂类
@Autowired
private ImpSatSecurity scurityFactory;
/**
* 是否需要安全验证
*
* @return
*/
public boolean isNeedValidate(ServerHttpRequest request) {
return isMatched(request, securityProperties.getNoExcludeUrls());
}
public int getOrder() {
return -90;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求体
ServerHttpRequest request = exchange.getRequest();
HttpMethod method = request.getMethod();
//开启验证开关
if("false".equals(securityProperties.getIsValidate())){
logger.info("----no need security filter----");
return chain.filter(exchange);
}
if (!isNeedValidate(request)) {
logger.info("----no need security filter----");
return chain.filter(exchange);
}
if (method == HttpMethod.POST || method == HttpMethod.PUT) {
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
if (contentType != null && contentType.equals(MediaType.APPLICATION_JSON)) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
try {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
String requestBody = new String(bytes, Charset.forName("UTF-8"));
SecurityRequest securityRequest = JSONObject.parseObject(requestBody, SecurityRequest.class);
if (securityRequest == null) {
chain.filter(exchange);
}
//解密请求参数
String decryptedRequestStr = decryptParams(securityRequest);
ServerHttpRequestDecorator newRequest = new ServerHttpRequestDecorator(request) {
@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 = decryptedRequestStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = new DefaultDataBufferFactory().wrap(bytes);
return Flux.just(buffer);
}
};
ServerHttpResponse originalResponse = exchange.getResponse();
HttpHeaders headers = new HttpHeaders();
headers.putAll(originalResponse.getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH);
SecurityRequest SecurityRequest = securityRequest;
ServerHttpResponse decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] byteArray = new byte[join.readableByteCount()];
join.read(byteArray);
String originalResponseBody = new String(byteArray, Charset.forName("UTF-8"));
//加密
byte[] encryptedByteArray = encryptResponse(originalResponseBody, SecurityRequest.getAesKey()).getBytes(StandardCharsets.UTF_8);
DataBuffer wrap = dataBufferFactory.wrap(encryptedByteArray);
DataBufferUtils.release(join);
return wrap;
}));
}
return super.writeWith(body);
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
};
decoratedResponse.getHeaders().addAll(headers);
ServerWebExchange newExchange = exchange.mutate().request(newRequest).response(decoratedResponse).build();
return chain.filter(newExchange);
} catch (Exception e) {
log.error("请求加解密错误");
e.printStackTrace();
} finally {
DataBufferUtils.release(dataBuffer);
}
return Mono.empty();
});
}
}
return chain.filter(exchange);
}
// 对参数进行解密
private String decryptParams(SecurityRequest securityRequest) {
//检查公共参数
if (securityRequest.checkParameter()) {
logger.error("IMP SAT GW securityRequest public parameter is null");
throw new BizException(BaseResponseEnum.ILLEGAL_REQUEST.getCode(), BaseResponseEnum.ILLEGAL_REQUEST.getMsg());
}
//对requestData进行解密
JSONObject requestData = scurityFactory.processParametersToJSONObj(securityRequest);
logger.info("IMP SAT GW requestData is {}", JSONObject.toJSONString(requestData));
return requestData.toJSONString();
}
private String encryptResponse(String responseContent, String aesKey) {
//统一加密处理返回
JSONObject jsonObject = JSONObject.parseObject(responseContent);
if (jsonObject == null || "".equals(jsonObject.toString())) {
jsonObject = new JSONObject();
}
String dataStr = JSONObject.toJSONString(jsonObject, SerializerFeature.DisableCircularReferenceDetect);
logger.info("response data={}", StringUtils.abbreviate(dataStr, 500));
JSONObject result = JSONObject.parseObject(dataStr);
//此处要注意,加密只处理data,所以返回的数据结构体要是ResponseModel
String resBodyStr = JSONObject.toJSONString(result.get("data"));
//更改返回内容
result.put("data", AESCBCUtils.encrypt(resBodyStr, aesKey));
return result.toJSONString();
}
public static boolean isMatched(ServerHttpRequest request, Set<String> list) {
if (CollectionUtils.isEmpty(list)) {
return false;
}
String uri = request.getURI().getPath();
for (String url : list) {
if (UrlPathUtils.match(url, uri)) {
return true;
}
}
return false;
}
}
总结:上述是本人在gateway实现接口加解密的整体实现过程,中间才到一个坑,ServerHttpRequestDecorator 在实例化时一定要实现getHeaders方法,写者在是实现时漏掉了这一步,导致下游一直等待报错:
@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;
}
2023-05-30 14:34:39.710 ERROR 28536 --- [http-nio-48089-exec-2] [TID: N/A] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
cn.hutool.core.io.IORuntimeException: ClientAbortException: java.net.SocketTimeoutException
at cn.hutool.core.io.copy.StreamCopier.copy(StreamCopier.java:71)
at cn.hutool.core.io.IoUtil.copy(IoUtil.java:162)
at cn.hutool.core.io.IoUtil.copy(IoUtil.java:146)
at cn.hutool.core.io.IoUtil.copy(IoUtil.java:132)
at cn.hutool.core.io.IoUtil.copy(IoUtil.java:119)
at cn.hutool.core.io.IoUtil.read(IoUtil.java:408)
at cn.hutool.core.io.IoUtil.readBytes(IoUtil.java:495)
at cn.hutool.core.io.IoUtil.readBytes(IoUtil.java:461)
at cn.hutool.extra.servlet.ServletUtil.getBodyBytes(ServletUtil.java:117)
at com.pharmaoryx.starter.web.core.filter.CacheRequestBodyWrapper.<init>(CacheRequestBodyWrapper.java:28)
at com.pharmaoryx.starter.web.core.filter.CacheRequestBodyFilter.doFilterInternal(CacheRequestBodyFilter.java:22)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at com.pharmaoryx.starter.env.core.web.EnvWebFilter.doFilterInternal(EnvWebFilter.java:28)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
source: //www.jianshu.com/p/2b9cbf030877