spring gateway 实现全信道加解密

前言:近期在项目中需要对指定请求整个信道进行加解密(防篡改,反拦截),实现整个信道通讯安全,由于不确定后期那些接口可能需要类似处理,最后决定放在网关实现该功能(虽然稍微占用网关的性能,但是能够全局统一处理十分方便,性能方面可以增加配置来解决)。此过程中也踩了些坑,在此记录下来,以便后来人参考;

首先我们采用的是GlobalFilter,GlobalFilter是应用于所有路由的特殊过滤器。
GlobalFilter接口的实现类如下图所示:

9a7587b5da12fd4664cfd6ee40392db6.png

e884c58ab7db44abfc0da512a293313e.png

当请求与路由匹配时,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

a008c23c8c972c48f0668e1fda740680.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值