springweb flux拦截请求获取参数和方法做接口签名防重放校验

news2024/12/23 14:17:40

在给spring webflux做接口签名、防重放的时候,往往需要获取请求参数,请求方法等,而spring webflux无法像spring mvc那样好获取,这里根据之前的实践特地说明一下:

总体思路:
1、利用过滤器,从原request中获取到信息后,缓存在一个上下文对象中,然后构造新的request,传入后面的过滤器。因为原request流式的,用过一次后便无法再取参数了。
2、通过exchange的Attributes传递上下文对象,在不同的过滤器中使用即可。

1、上下文对象

@Getter
@Setter
@ToString
public class GatewayContext {

    public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext";

    /**
     * cache requestMethod
     */
    private String requestMethod;

    /**
     * cache queryParams
     */
    private MultiValueMap<String, String> queryParams;

    /**
     * cache json body
     */
    private String requestBody;
    /**
     * cache Response Body
     */
    private Object responseBody;
    /**
     * request headers
     */
    private HttpHeaders requestHeaders;
    /**
     * cache form data
     */
    private MultiValueMap<String, String> formData;
    /**
     * cache all request data include:form data and query param
     */
    private MultiValueMap<String, String> allRequestData = new LinkedMultiValueMap<>(0);

    private byte[] requestBodyBytes;

}

2、在过滤器中获取请求参数、请求方法。
这里我们只对application/jsonapplication/x-www-form-urlencoded这种做body参数拦截,而对于其他的请求,则可以通过url直接获取到query参数。

@Slf4j
@Component
public class GatewayContextFilter implements WebFilter, Ordered {

    /**
     * default HttpMessageReader
     */
    private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        GatewayContext gatewayContext = new GatewayContext();
        HttpHeaders headers = request.getHeaders();
        gatewayContext.setRequestHeaders(headers);
        gatewayContext.getAllRequestData().addAll(request.getQueryParams());
        gatewayContext.setRequestMethod(request.getMethodValue().toUpperCase());
        gatewayContext.setQueryParams(request.getQueryParams());
        /*
         * save gateway context into exchange
         */
        exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext);
        MediaType contentType = headers.getContentType();
        if (headers.getContentLength() > 0) {
            if (MediaType.APPLICATION_JSON.equals(contentType)) {
                return readBody(exchange, chain, gatewayContext);

            }
            if (MediaType.APPLICATION_FORM_URLENCODED.equalsTypeAndSubtype(contentType)) {
                return readFormData(exchange, chain, gatewayContext);
            }
        }

        String path = request.getPath().value();
        if (!"/".equals(path)) {
            log.info("{} Gateway context is set with {}-{}", path, contentType, gatewayContext);
        }
        return chain.filter(exchange);
    }


    @Override
    public int getOrder() {
        return Integer.MIN_VALUE + 1;
    }


    /**
     * ReadFormData
     */
    private Mono<Void> readFormData(ServerWebExchange exchange, WebFilterChain chain, GatewayContext gatewayContext) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        return exchange.getFormData()
                .doOnNext(multiValueMap -> {
                    gatewayContext.setFormData(multiValueMap);
                    gatewayContext.getAllRequestData().addAll(multiValueMap);
                    log.debug("[GatewayContext]Read FormData Success");
                })
                .then(Mono.defer(() -> {
                    Charset charset = headers.getContentType().getCharset();
                    charset = charset == null ? StandardCharsets.UTF_8 : charset;
                    String charsetName = charset.name();
                    MultiValueMap<String, String> formData = gatewayContext.getFormData();
                    /*
                     * formData is empty just return
                     */
                    if (null == formData || formData.isEmpty()) {
                        return chain.filter(exchange);
                    }
                    log.info("1. Gateway Context formData: {}", formData);
                    StringBuilder formDataBodyBuilder = new StringBuilder();
                    String entryKey;
                    List<String> entryValue;
                    try {
                        /*
                         * repackage form data
                         */
                        for (Map.Entry<String, List<String>> entry : formData.entrySet()) {
                            entryKey = entry.getKey();
                            entryValue = entry.getValue();
                            if (entryValue.size() > 1) {
                                for (String value : entryValue) {
                                    formDataBodyBuilder
                                            .append(URLEncoder.encode(entryKey, charsetName).replace("+", "%20").replace("*", "%2A").replace("%7E", "~"))
                                            .append("=")
                                            .append(URLEncoder.encode(value, charsetName).replace("+", "%20").replace("*", "%2A").replace("%7E", "~"))
                                            .append("&");
                                }
                            } else {
                                formDataBodyBuilder
                                        .append(URLEncoder.encode(entryKey, charsetName).replace("+", "%20").replace("*", "%2A").replace("%7E", "~"))
                                        .append("=")
                                        .append(URLEncoder.encode(entryValue.get(0), charsetName).replace("+", "%20").replace("*", "%2A").replace("%7E", "~"))
                                        .append("&");
                            }
                        }
                    } catch (UnsupportedEncodingException e) {
                        log.error("GatewayContext readFormData error {}", e.getMessage(), e);
                    }
                    /*
                     * 1. substring with the last char '&'
                     * 2. if the current request is encrypted, substring with the start chat 'secFormData'
                     */
                    String formDataBodyString = "";
                    String originalFormDataBodyString = "";
                    if (formDataBodyBuilder.length() > 0) {
                        formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1);
                        originalFormDataBodyString = formDataBodyString;
                    }
                    /*
                     * get data bytes
                     */
                    byte[] bodyBytes = formDataBodyString.getBytes(charset);
                    int contentLength = bodyBytes.length;
                    gatewayContext.setRequestBodyBytes(originalFormDataBodyString.getBytes(charset));
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.putAll(exchange.getRequest().getHeaders());
                    httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
                    /*
                     * in case of content-length not matched
                     */
                    httpHeaders.setContentLength(contentLength);
                    /*
                     * use BodyInserter to InsertFormData Body
                     */
                    BodyInserter<String, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromObject(formDataBodyString);
                    CachedBodyOutputMessage cachedBodyOutputMessage = new CachedBodyOutputMessage(exchange, httpHeaders);
                    log.info("2. GatewayContext Rewrite Form Data :{}", formDataBodyString);
                    return bodyInserter.insert(cachedBodyOutputMessage, new BodyInserterContext())
                            .then(Mono.defer(() -> {
                                ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                                        exchange.getRequest()) {
                                    @Override
                                    public HttpHeaders getHeaders() {
                                        return httpHeaders;
                                    }

                                    @Override
                                    public Flux<DataBuffer> getBody() {
                                        return cachedBodyOutputMessage.getBody();
                                    }
                                };
                                return chain.filter(exchange.mutate().request(decorator).build());
                            }));
                }));
    }


    /**
     * ReadJsonBody
     */
    private Mono<Void> readBody(ServerWebExchange exchange, WebFilterChain chain, GatewayContext gatewayContext) {
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .flatMap(dataBuffer -> {
                    /*
                     * read the body Flux<DataBuffer>, and release the buffer
                     * when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature
                     * see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095
                     */
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    gatewayContext.setRequestBodyBytes(bytes);
                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                        DataBufferUtils.retain(buffer);
                        return Mono.just(buffer);
                    });
                    /*
                     * repackage ServerHttpRequest
                     */
                    ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };
                    ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
                    return ServerRequest.create(mutatedExchange, MESSAGE_READERS)
                            .bodyToMono(String.class)
                            .doOnNext(objectValue -> {
                                gatewayContext.setRequestBody(objectValue);
                                if (objectValue != null && !objectValue.trim().startsWith("{")) {
                                    return;
                                }
                                try {
                                    gatewayContext.getAllRequestData().setAll(JsonUtil.fromJson(objectValue, Map.class));
                                } catch (Exception e) {
                                    log.warn("Gateway context Read JsonBody error:{}", e.getMessage(), e);
                                }
                            }).then(chain.filter(mutatedExchange));
                });
    }

}

3、签名、防重放校验
这里我们从上下文对象中取出参数即可
签名算法逻辑:
在这里插入图片描述

@Slf4j
@Component
public class GatewaySignCheckFilter implements WebFilter, Ordered {


    @Value("${api.rest.prefix}")
    private String apiPrefix;

    @Autowired
    private RedisUtil redisUtil;

    //前后端约定签名密钥
    private static final String API_SECRET = "secret-xxx";

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE + 2;
    }

    @NotNull
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, @NotNull WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String uri = request.getURI().getPath();
        GatewayContext gatewayContext = (GatewayContext) exchange.getAttributes().get(GatewayContext.CACHE_GATEWAY_CONTEXT);
        HttpHeaders headers = gatewayContext.getRequestHeaders();
        MediaType contentType = headers.getContentType();
        log.info("check url:{},method:{},contentType:{}", uri, gatewayContext.getRequestMethod(), contentType == null ? "" : contentType.toString());
        //如果contentType为空,只能是get请求
        if (contentType == null || StringUtils.isBlank(contentType.toString())) {
            if (request.getMethod() != HttpMethod.GET) {
                throw new RuntimeException("非法访问");
            }
            checkSign(uri, gatewayContext, exchange);
        } else {
            if (MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_FORM_URLENCODED.equalsTypeAndSubtype(contentType)) {
                checkSign(uri, gatewayContext, exchange);
            }
        }

        return chain.filter(exchange);
    }


    private void checkSign(String uri, GatewayContext gatewayContext, ServerWebExchange exchange) {
        //忽略掉的请求
        List<String> ignores = Lists.newArrayList("/open/**", "/open/login/params", "/open/image");
        for (String ignore : ignores) {
            ignore = apiPrefix + ignore;
            if (uri.equals(ignore) || uri.startsWith(ignore.replace("/**", "/"))) {
                log.info("check sign ignore:{}", uri);
                return;
            }
        }
        String method = gatewayContext.getRequestMethod();
        log.info("start check sign {}-{}", method, uri);
        HttpHeaders headers = gatewayContext.getRequestHeaders();
        log.info("headers:{}", JsonUtils.objectToJson(headers));
        String clientId = getHeaderAttr(headers, SystemSign.CLIENT_ID);
        String timestamp = getHeaderAttr(headers, SystemSign.TIMESTAMP);
        String nonce = getHeaderAttr(headers, SystemSign.NONCE);
        String sign = getHeaderAttr(headers, SystemSign.SIGN);
        checkTime(timestamp);
        checkOnce(nonce);
        String headerStr = String.format("%s=%s&%s=%s&%s=%s", SystemSign.CLIENT_ID, clientId,
                SystemSign.NONCE, nonce, SystemSign.TIMESTAMP, timestamp);
        String signSecret = API_SECRET;
        String queryUri = uri + getQueryParam(gatewayContext.getQueryParams());
        log.info("headerStr:{},signSecret:{},queryUri:{}", headerStr, signSecret, queryUri);
        String realSign = calculatorSign(clientId, queryUri, gatewayContext, headerStr, signSecret);
        log.info("sign:{}, realSign:{}", sign, realSign);
        if (!realSign.equals(sign)) {
            log.warn("wrong sign");
            throw new RuntimeException("Illegal sign");
        }
    }

    private String getQueryParam(MultiValueMap<String, String> queryParams) {
        if (queryParams == null || queryParams.size() == 0) {
            return StringUtils.EMPTY;
        }
        StringBuilder builder = new StringBuilder("?");
        for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
            String key = entry.getKey();
            List<String> value = entry.getValue();
            builder.append(key).append("=").append(value.get(0)).append("&");
        }
        builder.deleteCharAt(builder.length() - 1);
        return builder.toString();
    }

    private String getHeaderAttr(HttpHeaders headers, String key) {
        List<String> values = headers.get(key);
        if (CollectionUtils.isEmpty(values)) {
            log.warn("GatewaySignCheckFilter empty header:{}", key);
            throw new RuntimeException("GatewaySignCheckFilter empty header:" + key);
        }
        String value = values.get(0);
        if (StringUtils.isBlank(value)) {
            log.warn("GatewaySignCheckFilter empty header:{}", key);
            throw new RuntimeException("GatewaySignCheckFilter empty header:" + key);
        }
        return value;
    }


    private String calculatorSign(String clientId, String queryUri, GatewayContext gatewayContext, String headerStr, String signSecret) {
        String method = gatewayContext.getRequestMethod();
        byte[] bodyBytes = gatewayContext.getRequestBodyBytes();
        if (bodyBytes == null) {
            //空白的md5固定为:d41d8cd98f00b204e9800998ecf8427e
            bodyBytes = new byte[]{};
        }
        String bodyMd5 = UaaSignUtils.getMd5(bodyBytes);
        String ori = String.format("%s\n%s\n%s\n%s\n%s\n", method, clientId, headerStr, queryUri, bodyMd5);
        log.info("clientId:{},signSecret:{},headerStr:{},bodyMd5:{},queryUri:{},ori:{}", clientId, signSecret, headerStr, bodyMd5, queryUri, ori);
        return UaaSignUtils.sha256HMAC(ori, signSecret);
    }

    private void checkOnce(String nonce) {
        if (StringUtils.isBlank(nonce)) {
            log.warn("GatewaySignCheckFilter checkOnce Illegal");
        }
        String key = "api:auth:" + nonce;
        int fifteenMin = 60 * 15 * 1000;
        Boolean succ = redisUtil.setNxWithExpire(key, "1", fifteenMin);
        if (succ == null || !succ) {
            log.warn("GatewaySignCheckFilter checkOnce Repeat");
            throw new RuntimeException("checkOnce Repeat");
        }
    }


    private void checkTime(String timestamp) {
        long time;
        try {
            time = Long.parseLong(timestamp);
        } catch (Exception ex) {
            log.error("GatewaySignCheckFilter checkTime error:{}", ex.getMessage(), ex);
            throw new RuntimeException("checkTime error");
        }
        long now = DateTimeUtil.now();
        log.info("now: {}, time: {}", DateTimeUtil.millsToStr(now), DateTimeUtil.millsToStr(time));
        int fiveMinutes = 60 * 5 * 1000;
        long duration = now - time;
        if (duration > fiveMinutes || (-duration) > fiveMinutes) {
            log.warn("GatewaySignCheckFilter checkTime Late");
            throw new RuntimeException("checkTime Late");
        }
    }

    public interface SystemSign {
        /**
         * 客户端ID:固定值,由后端给前端颁发约定
         */
        String CLIENT_ID = "client-id";

        /**
         * 客户端计算出的签名
         */
        String SIGN = "sign";

        /**
         * 时间戳
         */
        String TIMESTAMP = "timestamp";

        /**
         * 唯一值
         */
        String NONCE = "nonce";
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1153223.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Yuhan Blu-ray DVD Creator for Mac: 打造专属的高清视听盛宴

在如今的高清时代&#xff0c;谁能拒绝一款能够轻松将高清影片刻录成蓝光DVD的刻录机呢&#xff1f;而Yuhan Blu-ray DVD Creator for Mac正是这样一款令人惊艳的软件。 作为一款专为Mac用户打造的蓝光DVD刻录机&#xff0c;Yuhan Blu-ray DVD Creator for Mac支持将各种高清视…

涛思数据与胜软科技达成战略合作,共同赋能石油行业数字化转型

近日&#xff0c;北京涛思数据科技有限公司&#xff08;以下简称“涛思数据”&#xff09;与山东胜软科技股份有限公司&#xff08;以下简称“胜软科技”&#xff09;于山东东营签署战略合作协议。双方围绕石油行业数字化转型趋势&#xff0c;合力打造石油行业数字化转型解决方…

设计模式_观察者模式

观察者模式 介绍 设计模式定义案例问题堆积在哪里解决办法观察者是行为型设计模式 多个对象 观察 1个对象小强考试完 成绩公布了 家长/同学得知成绩后 做出不同反应一个一个通知很麻烦 先通知谁 也有讲究的 信息发布方 抽象出一个信息管理类 负责管理监听者 类图 代码 Obse…

【Apache Flink】Flink DataStream API的基本使用

Flink DataStream API的基本使用 文章目录 前言1. 基本使用方法2. 核心示例代码3. 完成工程代码pom.xmlWordCountExample测试验证 4. Stream 执行环境5. 参考文档 前言 Flink DataStream API主要用于处理无界和有界数据流 。 无界数据流是一个持续生成数据的数据源&#xff0…

如何理解 Spring Boot 中的 Starter?

Starter 是 Spring Boot 的四大核心功能特性之一&#xff0c;除此之外,Spring Boot 还有自动装配、Actuator 监控等特性。Spring Boot 里面的这些特性&#xff0c;都是为了让开发者在开发基于 Spring 生态下的企业级应用时&#xff0c;只需要关心业务逻辑&#xff0c;减少对配置…

初识FFmpeg

前言 无意间见到群里的小伙伴展示视频工具。功能比较多&#xff0c;包括视频编码修改&#xff0c;画质处理&#xff0c;比例处理、名称提取&#xff0c;剪辑、标题拆解。因此开始了FFmpeg学习。以下摘自百度百科的解释。 FFmpeg是一套可以用来记录、转换数字音频、视频&#xf…

【LVS实战】02 搭建一个LVS-NAT实验

一、网络结构 用虚拟机搭建如下的几台机器&#xff0c;并配置如下的ip 关于虚拟机网卡和网络的配置&#xff0c;可以参考 iptables章节&#xff0c;05节&#xff1a;网络转发实验 主机A模拟外网的机器 B为负载均衡的机器 C和D为 RealServer 二、C和D主机的网关设置 C和D机…

vue项目引入elementui样式组件05

vue前端开发&#xff0c;关于样式部分&#xff0c;不需要自己去写&#xff0c;可以引用现有的一些组件&#xff0c;比如elemtnui&#xff0c;可官网查看 1、下载对应的包到vue项目中 通过npm进行安装 npm i element-ui -S2、引入到项目中&#xff0c;官网也提供了例子 3、运…

Shopee流量和销量不佳?或许你没有掌握正确的引流方法

很多卖家做了很久&#xff0c;但是发现流量和销量都没怎么增长&#xff0c;今天陈哥就分享一下如何正确的引流。 以下是一些有效的引流策略&#xff1a; 1. 站内引流&#xff1a;选择高性价比的潮流商品&#xff0c;根据目标客户群和重点品类进行选品。优化商品名称和描述&am…

顺序表练习

顺序表练习 图解插入与删除&#xff0c;详见相关内容&#xff1a;顺序存储结构的插入与删除 //顺序表的定义、创建、插入、删除、查找 //定义&#xff1a;结构体中数组、表长 //创建:输入元素&#xff0c;表长 //插入&#xff1a;判断表是否已满、判断位序合法性 //插入位序k…

好用的视频下载工具推荐

我不允许还有人不知道这款视频下载工具, 真的太好用了! &#xff01;! 随着视频行业的崛起&#xff0c;如今网络上各种各样的视频层出不穷, 那我们看到喜欢的视频该如何下载呢&#xff1f;今天小编来给大家分享一款非常实用的视频下载工具——Downni, 它兼容国内外大多数视频网…

网络编程服务端与客户端存在的端口问题

服务端的窗口不能再次使用的原因如下&#xff1a; 服务器端的窗口不能再次使用的原因可能有以下几点&#xff1a; 1. 窗口已经关闭&#xff1a;如果服务器端的窗口已经被关闭&#xff0c;那么就无法再次使用。关闭窗口后&#xff0c;服务器会释放相关资源&#xff0c;包括与该…

深度学习入门(二)之神经网络

文章目录 从感知机到神经网络神经网络的例子复习感知机激活函数 激活函数sigmoid函数阶跃函数的实现阶跃函数的图形sigmoid函数的图形sigmoid函数与阶跃函数比较非线性函数ReLU函数 多维数组的运算多维数组矩阵乘法神经网络的内积 三层神经网络的实现符号确认各层间信号传递的实…

视频剪辑达人教您:如何运用嵌套合并技巧制作固定片尾

在视频剪辑的过程中&#xff0c;嵌套合并技巧是一种非常实用的技术&#xff0c;可以帮助您将多个素材叠加在一起&#xff0c;制作出更加丰富多彩的视频。本文将由视频剪辑达人为您详细介绍如何运用云炫AI智剪嵌套合并技巧制作固定片尾&#xff0c;让您的视频剪辑水平更上一层楼…

场景交易额超40亿,海尔智家三翼鸟开始收获

文 | 螳螂观察 作者 | 余一 随着双十一的到来&#xff0c;国内的消费情绪再次被点燃。在这类大促之下&#xff0c;品牌们就像一个个天体&#xff0c;不断引动着市场潮汐&#xff0c;期待自己能触发更大的“海潮效应”。 所谓“海潮效应”是指&#xff0c;海水因天体的引力而…

总结之数据分析工具cube.js通过Docker部署

cube.js介绍 官网地址&#xff1a;https://cube.dev/ Cube.js是一个开源的模块化框架&#xff0c;用于构建分析web应用程序。它主要用于构建内部业务智能工具或向现有应用程序添加面向客户的分析。 Cube.js设计用于无服务器查询引擎&#xff0c;如AWS Athena和谷歌BigQuery。…

一张动图告诉你,输入网址之后,发生了什么事情?

让我们一步一步地来看这个过程。 步骤1&#xff1a; 用户在浏览器中输入一个URL&#xff08;比如www.bytebytego.com&#xff09;&#xff0c;然后按下回车键。首先&#xff0c;我们需要将这个URL转换成一个IP地址。通常&#xff0c;这个映射关系会被存储在缓存中&#xff0…

【设计模式】第6节:创建型模式之“原型模式”

由于本人现在所使用的语言主要是golang&#xff0c;所以后面的代码主要使用golang编写。语言实现应该不是障碍&#xff0c;主要是理解每种设计模式它的思想。 如果对象的创建成本比较大&#xff0c;而同一个类的不同对象之间差别不大&#xff08;大部分字段都相同&#xff09;…

企业 Tomcat 运维 部署tomcat反向代理集群

一、Tomcat 简介 Tomcat服务器是一个免费的开放源代码的Web应用服务器&#xff0c;属于轻量级应用服务器&#xff0c; Tomcat和Nginx、Apache(httpd)、Web服务器一样&#xff0c;具有处理HTML页面的功能不过Tomcat处理静态HTML的能力不如Nginx/Apache服务器 一个tomcat默认并…