• 【SpringCloud】Spring Cloud Gateway实现接口防篡改


    Spring Cloud Gateway实现接口防篡改和防重放

    Spring Cloud Gateway 是基于 Spring Framework 5.0 和 Spring Boot 2.0 构建的 API 网关,提供路由等功能。其旨在提供一种简单而有效的方法路由到 API,并为它们提供跨领域的关注点,例如:安全性、监视 / 指标和弹性。



    前言

    .在云计算、大数据等技术日趋成熟的情况下,微服务架构逐渐进入人们的视线,微服务架构的本质是把整体的业务拆分成特定明确功能的服务,在分布式环境下,随着微服务架构的广泛应用,各个服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务,这些服务之间的调用非常复杂,为了配合新核心建设,保障系统间系统传递的安全性,需要设计报文防篡改的技术。


    一、防篡改是什么?

    防篡改(英语:Tamper resistance)是指通过包装、系统或其他物理措施抗击产品正常用户的篡改(tamper,故意引发故障或造成破坏)的行为。

    二、防重放是什么?

    入侵者 C 可以从网络上截获 A 发给 B 的报文。C 并不需要破译这个报文(因为这
    可能很花很多时间)而可以直接把这个由 A 加密的报文发送给 B,使 B 误认为 C 就是 A。然后
    B 就向伪装是 A 的 C 发送许多本来应当发送给 A 的报文

    三.防篡改和防重放的解决方式

    1. 可以通过时间戳,将时间戳放在header头中进行处理
    2. 通过时间戳 + sign签名处理,通过将报文参数进行相应的md5进行签名处理,同时将时间戳和sign放在header中,网关进行相应的验签证明请求的合法性
    3. 通过时间戳+随机数(norce)+sign签名的方式进行处理
      流程如下:
      在这里插入图片描述
    • 用户发送请求,带上时间戳,随机数和sign数据
    • 请求到网关,网关校验相关的数据
    • 如果校验失败是直接返回,校验成功就跳转到后台的业务系统服务中

    四.Spring Cloud Gateway实现

    Spring Cloud Gateway是通过webflux实现的,我们现在进行代码coding

    创建过滤器SignatureAntiReplayFilter

    代码如下:

    @Slf4j
    @Component
    public class SignatureAntiReplayFilter implements WebFilter, Ordered {
    
        @Resource
        private ReactiveCredentialsFilter reactiveCredentialsFilter;
    	//设置一个开关,进行控制是否开启报文签名校验
        @Value("${gateway.openSign:#{true}}")
        private Boolean openSign;
    
        @Override
        public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
            log.info("===========签名校验过滤器======");
           	/***
           	**
           	* 这里面还可以添加其他的业务逻辑在里面
           	*/
            //如果不开启的话就直接放行
            if (Boolean.FALSE.equals(openSign)) {
                return webFilterChain.filter(serverWebExchange);
            }
            return SignatureAntiReplayMono.mono(serverWebExchange, webFilterChain);
        }
    
        @Override
        public int getOrder() {
            return -2;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    创建SignatureAntiReplayMono处理类

    代码如下:

    @Slf4j
    public class SignatureAntiReplayMono {
    
        private SignatureAntiReplayMono() {
        }
    
        /**
         * 处理过滤器
         *
         * @param serverWebExchange serverWebExchange
         * @param webFilterChain    过滤器链
         * @return 返回mono
         */
        public static Mono<Void> mono(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
            ServerHttpResponse serverHttpResponse = serverWebExchange.getResponse();
            ServerHttpRequest serverHttpRequest = serverWebExchange.getRequest();
            HttpHeaders httpHeaders = serverWebExchange.getRequest().getHeaders();
            List<String> contentType = httpHeaders.get(HttpHeaders.CONTENT_TYPE);
            if (!CollectionUtils.isEmpty(contentType) && !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
                Result<Void> checkMap = AccessRequestCheck.checkParam(serverWebExchange.getRequest().getHeaders());
                if (Objects.equals(String.valueOf(HttpStatus.OK.value()), checkMap.getCode())) {
                    return webFilterChain.filter(serverWebExchange);
                } else {
                    try {
                        return serverHttpResponse.writeWith(Flux.just(serverHttpResponse.bufferFactory().wrap(ObjectMapperUtil.getObjectMapper().writeValueAsString(checkMap).getBytes(StandardCharsets.UTF_8))));
                    } catch (JsonProcessingException e) {
                        return Mono.empty();
                    }
                }
            }
            ServerRequest serverRequest = ServerRequest.create(serverWebExchange, HandlerStrategies.withDefaults().messageReaders());
            Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(originalBody -> modifyBody()
                    .apply(serverWebExchange, originalBody));
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(serverWebExchange.getRequest().getHeaders());
            headers.remove("Content-Length");
            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
            CusCachedBodyOutputMessage outputMessage = new CusCachedBodyOutputMessage(serverWebExchange, headers);
            return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                ServerHttpRequest decorator = decorate(serverWebExchange, headers, outputMessage);
                return webFilterChain.filter(serverWebExchange.mutate().request(decorator).build());
            })).onErrorResume((throwable) -> {
                return release(outputMessage, (Throwable) throwable);
            });
        }
    
        protected static Mono<Void> release(CusCachedBodyOutputMessage outputMessage, Throwable throwable) {
            return Boolean.TRUE.equals(outputMessage.isCached()) ? outputMessage.getBody().map(DataBufferUtils::release).then(Mono.error(throwable)) : Mono.error(throwable);
        }
    
        static ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CusCachedBodyOutputMessage outputMessage) {
            return new ServerHttpRequestDecorator(exchange.getRequest()) {
                public HttpHeaders getHeaders() {
                    long contentLength = headers.getContentLength();
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.putAll(headers);
                    if (contentLength > 0L) {
                        httpHeaders.setContentLength(contentLength);
                    } else {
                        httpHeaders.set("Transfer-Encoding", "chunked");
                    }
                    return httpHeaders;
                }
    
                public Flux<DataBuffer> getBody() {
                    return outputMessage.getBody();
                }
            };
        }
    
        private static BiFunction<ServerWebExchange, String, Mono<String>> modifyBody() {
            return (serverWebExchange, raw) -> {
                try {
                    if (StringUtils.isBlank(raw)) {
                        raw = "{}";
                    }
                    Result<Void> checkMap = AccessRequestCheck.checkParam(serverWebExchange.getRequest().getHeaders(), raw);
                    if (Objects.equals(String.valueOf(HttpStatus.OK.value()), checkMap.getCode())) {
                        return Mono.just(raw);
                    } else {
                        return Mono.error();
                    }
                } catch (Exception e) {
                    return Mono.error();
                }
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    创建相关的AccessRequestCheck类

    代码如下:

     /**
         * 请求过期时间
         */
        public static final long EXPIRE_TIME = 60000;
    
        /**
         * 时间戳属性
         */
        public static final String TIMESTAMP_PROPERTIES = "timestamp";
    
        /**
         * 签名属性
         */
        public static final String SIGN_PROPERTIES = "sign";
    
        /**
         * 盐
         */
        public static final String NORCE_PROPERTIES = "norce";
    
    
        public static StringRedisTemplate redisTemplate;
    
        @Autowired
        public void setRedisTemplate(StringRedisTemplate redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        public static Result<Void> checkParam(HttpHeaders headers, String body) {
            Result<Void> result = new Result<>();
            //获取header中的时间戳
            List<String> timestamp = headers.get(TIMESTAMP_PROPERTIES);
            //获取uuid 这里设置为盐
            List<String> uuid = headers.get(NORCE_PROPERTIES);
            //从header头中添加签名,签名使用请求参数进行加密生成sign
            List<String> sign = headers.get(SIGN_PROPERTIES);
            if (CollectionUtils.isEmpty(timestamp)) {
                result.setCode(ErrorCodeEnum.TIMESTAMP_PARAM_MISSING.getCode());
                result.setMsg(ErrorCodeEnum.TIMESTAMP_PARAM_MISSING.getMsg());
                return result;
            }
            if (CollectionUtils.isEmpty(uuid)) {
                result.setCode(ErrorCodeEnum.NORCE_PROPERTIES.getCode());
                result.setMsg(ErrorCodeEnum.NORCE_PROPERTIES.getMsg());
                return result;
            }
            if (CollectionUtils.isEmpty(sign)) {
                result.setCode(ErrorCodeEnum.SIGN_PARAM_MISSING.getCode());
                result.setMsg(ErrorCodeEnum.SIGN_PARAM_MISSING.getMsg());
                return result;
            }
            //判断请求是否过期
            if (!checkExpire(Long.parseLong(timestamp.get(0)))) {
                result.setCode(ErrorCodeEnum.REQUEST_EXPIRE.getCode());
                result.setMsg(ErrorCodeEnum.REQUEST_EXPIRE.getMsg());
                return result;
            }
            //判断是否重复请求
            if (Objects.nonNull(redisTemplate.opsForValue().get(uuid.get(0)))) {
                result.setCode(ErrorCodeEnum.REQUEST_REPLACE.getCode());
                result.setMsg(ErrorCodeEnum.REQUEST_REPLACE.getMsg());
                return result;
            }
            //校验签名
            if (!SignUtil.verifySign(body, sign.get(0), uuid.get(0))) {
                result.setCode(ErrorCodeEnum.SIGN_ERROR.getCode());
                result.setMsg(ErrorCodeEnum.SIGN_ERROR.getMsg());
                return result;
            }
    
            redisTemplate.opsForValue().set(uuid.get(0), sign.get(0), EXPIRE_TIME, TimeUnit.MILLISECONDS);
            result.setCode(String.valueOf(HttpStatus.OK.getCode()));
            result.setMsg(HttpStatus.OK.getMessageEn());
            return result;
        }
    
        /**
         * 对时间戳和uuid进行校验
         *
         * @param headers 请求头
         * @return
         */
        public static Result<Void> checkParam(HttpHeaders headers) {
            Result<Void> result = new Result<>();
            //获取header中的时间戳
            List<String> timestamp = headers.get(TIMESTAMP_PROPERTIES);
            //获取uuid 这里设置为盐
            List<String> uuid = headers.get(UUID_PROPERTIES);
            if (CollectionUtils.isEmpty(timestamp)) {
                result.setCode(ErrorCodeEnum.TIMESTAMP_PARAM_MISSING.getCode());
                result.setMsg(ErrorCodeEnum.TIMESTAMP_PARAM_MISSING.getMsg());
                return result;
            }
            if (CollectionUtils.isEmpty(uuid)) {
                result.setCode(ErrorCodeEnum.NORCE_PROPERTIES.getCode());
                result.setMsg(ErrorCodeEnum.NORCE_PROPERTIES.getMsg());
                return result;
            }
            //判断请求是否过期
            if (!checkExpire(Long.parseLong(timestamp.get(0)))) {
                result.setCode(ErrorCodeEnum.REQUEST_EXPIRE.getCode());
                result.setMsg(ErrorCodeEnum.REQUEST_EXPIRE.getMsg());
                return result;
            }
            //判断是否重复请求
            if (Objects.nonNull(redisTemplate.opsForValue().get(uuid.get(0)))) {
                result.setCode(ErrorCodeEnum.REQUEST_REPLACE.getCode());
                result.setMsg(ErrorCodeEnum.REQUEST_REPLACE.getMsg());
                return result;
            }
    
            redisTemplate.opsForValue().set(uuid.get(0), uuid.get(0), EXPIRE_TIME, TimeUnit.MILLISECONDS);
            result.setCode(String.valueOf(HttpStatus.OK.getCode()));
            result.setMsg(HttpStatus.OK.getMessageEn());
            return result;
        }
    
        /**
         * 检测请求时间是否过期
         *
         * @param requestTime 请求时间
         * @return 返回检测结果
         */
        private static boolean checkExpire(long requestTime) {
            return EXPIRE_TIME > System.currentTimeMillis() - requestTime;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126

  • 相关阅读:
    聊聊交互设计师的成长 优漫动游
    ubuntu linux 环境下的程序打包
    例39:使用List控件
    ES6生成器(Generator)和迭代器(Iterator)
    大学生餐饮主题网页制作 美食网页设计模板 学生静态网页作业成品 dreamweaver美食甜品蛋糕HTML网站制作
    leetcode:188. 买卖股票的最佳时机IV
    3种工厂模式
    vue+element作用域插槽
    计算机视觉与深度学习 | 惯性/视觉/激光雷达SLAM技术综述
    红米k40s和红米note11pro哪个值得买 两者配置对比
  • 原文地址:https://blog.csdn.net/qian1314520hu/article/details/126177811