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

本文详细介绍了如何在SpringCloudGateway中通过时间戳、签名验证和Redis缓存来确保API安全,包括防篡改和防重放技术的实现步骤及关键代码片段。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
    }
}

创建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();
            }
        };
    }
}

创建相关的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;
    }

<think>好的,用户想了解在Spring Cloud Gateway实现鉴权和验签的最佳实践。我需要根据提供的引用内容来整理回答。首先,鉴权通常涉及验证用户身份和权限,而验签则是验证请求的完整性和来源,防止数据篡改。 引用[1]提到使用统一网关鉴权,包含认证授权和白名单功能,其他服务专注于业务逻辑。这说明鉴权应该在网关层集中处理,这样可以避免每个服务重复实现。引用[2]指出通过GlobalFilter实现鉴权过滤,这应该是核心方法。引用[3]提到了开发openApi统一鉴权,使用自定义过滤器和配置,可能需要结合断言和过滤器链。 接下来,我需要分步骤说明如何实现。首先是鉴权部分,应该使用GlobalFilter,在过滤器中获取请求信息,检查白名单,验证Token,可能需要调用认证服务。然后是验签,这部分需要获取请求参数,按照约定规则生成签名,并与客户端传来的签名对比。同时要注意处理请求体多次读取的问题,比如使用缓存RequestBody的装饰类。 另外,配置白名单和签名规则在yml中,通过自定义配置类加载。异常处理方面,需要捕获鉴权失败和验签失败,返回相应的HTTP状态码和错误信息。 性能优化方面,可以提到缓存、异步处理和签名算法选择。最后,引用中的示例代码需要转换为Java代码,比如GlobalFilter的实现,验签的步骤等。确保各部分逻辑清晰,符合Spring Cloud Gateway的工作流程。</think>在Spring Cloud Gateway实现鉴权与验签的最佳实践可分为以下核心步骤,结合统一网关设计模式和过滤器机制实现安全管控: --- ### 一、鉴权实现方案 1. **使用GlobalFilter统一拦截请求** ```java @Component public class AuthFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); // 1. 检查白名单 if (isWhiteList(request.getPath().value())) { return chain.filter(exchange); } // 2. 验证Token String token = extractToken(request); return validateToken(token).flatMap(valid -> { if (valid) { return chain.filter(exchange); } else { return unauthorizedResponse(exchange); } }); } } ``` 通过白名单机制放行无需鉴权的接口(如登录、健康检查)[^1],通过JWT或OAuth2 Token验证用户身份[^2]。 2. **配置白名单与权限规则** ```yaml spring: cloud: gateway: routes: - id: auth-service uri: lb://auth-service predicates: - Path=/api/auth/** filters: - AuthFilter ``` --- ### 二、验签实现方案 1. **自定义验签过滤器** ```java public class SignVerifyFilter extends AbstractGatewayFilterFactory<SignVerifyFilter.Config> { @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); // 1. 获取签名参数 String sign = request.getHeaders().getFirst("X-Sign"); // 2. 生成服务端签名 String serverSign = generateSign(request.getQueryParams(), request.getBody()); // 3. 比对签名 if (sign.equals(serverSign)) { return chain.filter(exchange); } else { exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST); return exchange.getResponse().setComplete(); } }; } } ``` 使用HMAC-SHA256等算法生成签名,确保请求未被篡改[^3]。 2. **处理请求体重复读取** ```java // 通过装饰器缓存请求体 DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); String body = new String(bytes); exchange.getAttributes().put("cachedBody", body); return chain.filter(exchange); }); ``` --- ### 三、安全增强措施 1. **防御重放攻击** - 在签名参数中添加时间戳 - 服务端校验时间戳有效期(如5分钟) 2. **密钥管理** - 使用配置中心动态更新密钥 - 不同客户端分配独立密钥 --- ### 四、异常处理方案 ```java @Bean public ErrorWebExceptionHandler signCheckExceptionHandler() { return new JsonExceptionHandler(); } public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler { @Override protected Mono<ServerResponse> renderErrorResponse(ServerWebExchange exchange) { return ServerResponse.status(HttpStatus.UNAUTHORIZED) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue( Map.of("code": 401, "message": "签名校验失败") )); } } ``` --- ### 五、性能优化建议 1. **缓存机制**:对频繁验证的Token进行本地缓存 2. **异步处理**:使用Reactor异步编程模型 3. **算法选择**:优先使用SHA256代替更耗时的算法 ---
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值