Spring Cloud Gateway路由到Amazon S3签名失败处理

Spring Cloud Gateway路由到Amazon S3签名失败处理

背景

最近在预研统一存储网关,想到就是使用Spring Cloud Gateway作为网关的入口,再反向代理到S3对象存储服务器。

软件版本

网关:Spring Cloud Gateway 3.1.2

s3对象存储:minio

aws java sdk:1.12.429

问题现象

Spring Cloud Gateway的路由配置规则如下:

spring:
  cloud:
    gateway:
      routes:
        - id: s3-route
          uri: s3-endpiont
          predicates:
            - Path=/s3/**
          filters:
            - StripPrefix=1
            - PreserveHostHeader

我添加了两个过滤器,一个是StripPrefix这个过滤器,它有一个parts参数,它的作用是重新设置路由后的路径,比如我的请求是 gateway-host/s3/,parts参数设置为1的话,路由之后的路径会变成s3-endpiont,它会截取掉请求路径中的前缀,这个可以保证我们能够路由到准确的s3-endpoint地址。

还有一个过滤器是PreserveHostHeader,这个过滤器的作用是保留请求的Host头,如果不设置的话,请求经过网关路由之后Host头会变成uri对应的地址,这个也会导致S3签名校验失败,这边可以参考nginx转发到Amazon S3的配置,参考地址:https://stackoverflow.com/questions/53833505/nginx-confg-issue-couldnt-connect-to-s3-compatible-storage-from-nodejs-test-p

sdk调用如下,获取所有桶的接口:

public static void main(String[] args) {
  ClientConfiguration config = new ClientConfiguration().withProtocol(Protocol.HTTP);
  config.setSignerOverride("S3SignerType");
  AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
    .withClientConfiguration(config)
    .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("minioadmin", "minioadmin")))
    .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://10.1.140.3:8777/s3", null))
    .build();
  final List<Bucket> buckets = amazonS3.listBuckets();
  System.out.println(buckets);
}

在客户端调用请求时,会抛出异常,然后带着问题去搜索了一下解决方案。
在这里插入图片描述

原因以及解决方案

先Google了一下,没有找到合适的解决方案,StackOverFlow上面有类似的问题,参考:https://stackoverflow.com/questions/75834957/spring-cloud-gateway-to-s3-signaturedoesnotmatch/76097374,但是没有人回答(下面那个答案是我后来加上的~)。

然后把问题现象和ChatGPT描述了一下,得到了一些答案:
在这里插入图片描述
想到可以是StripPrefix过滤器修改了’Host’请求头,所以将StripPrefix过滤器去掉,最后配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: s3-route
          uri: s3-endpiont
          predicates:
            - Path=/**
          filters:
            - PreserveHostHeader

sdk调用:

public static void main(String[] args) {
    ClientConfiguration config = new ClientConfiguration().withProtocol(Protocol.HTTP);
    config.setSignerOverride("S3SignerType");
    AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
            .withClientConfiguration(config)
            .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("minioadmin", "minioadmin")))
            .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://10.1.140.3:8777", null))
            .build();
    final List<Bucket> buckets = amazonS3.listBuckets();
    System.out.println(buckets);
}

果然这次调用成功了:
在这里插入图片描述
但是按照上面的解决方案,不能够自定义访问的路径,其实还没有完全解决我的问题,再问一次ChatGPT:
在这里插入图片描述
回答中提到用自定义过滤器使用正确的’Host’头重新生成一份签名,我参考了GPT的回答还有查询了一些资料,写了生成签名的过滤器,代码参考如下(这边使用的签名算法是V2版本的,不同的版本应该需要不同的适配):

/**
 * 重新生成S3签名过滤器
 *
 * @author yuanzhihao
 * @since 2023/5/5
 */
@Component
@Slf4j
public class AWSSignGatewayFilterFactory extends AbstractGatewayFilterFactory<AWSSignGatewayFilterFactory.Config> {

    public AWSSignGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            DefaultRequest<Void> defaultRequest = regenerateAuthorization(config, exchange);
            ServerHttpRequest request = exchange.getRequest().mutate()
                    .headers(httpHeaders -> {
                        httpHeaders.set("Authorization", defaultRequest.getHeaders().get("Authorization"));
                        httpHeaders.set(Headers.DATE, defaultRequest.getHeaders().get(Headers.DATE));
                    })
                    .build();
            return chain.filter(exchange.mutate().request(request).build());
        };
    }


    // 重新计算并设置签名
    private DefaultRequest<Void> regenerateAuthorization(Config config, ServerWebExchange exchange) {
        AWSCredentials credentials = new BasicAWSCredentials(config.getAk(), config.getSk());
        DefaultRequest<Void> request = new DefaultRequest<>("Amazon S3");
        request.addHeader("Host", config.getEndpoint());
        // 这边把请求头全部带下去
        exchange.getRequest().getQueryParams().forEach((key, value) -> request.addParameter(key, value.get(0)));
        exchange.getRequest().getHeaders().forEach((key, value) -> request.addHeader(key, value.get(0)));
        String path = exchange.getRequest().getURI().getPath();
        String method = Objects.requireNonNull(exchange.getRequest().getMethod(), "Method is null").toString();
        request.setResourcePath(path);
        try {
            request.setEndpoint(new URI(config.getEndpoint()));
        } catch (URISyntaxException e) {
            log.error("URI error", e);
            throw new RuntimeException(e);
        }
        S3Signer signer = new S3Signer(method, path);
        signer.sign(request, credentials);
        return request;
    }


    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("endpoint", "ak", "sk");
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Config {
        private String endpoint;
        private String ak;
        private String sk;
    }
}

配置文件中生效过滤器:

s3:
  endpoint: http://10.1.140.3:9000
  ak: minioadmin
  sk: minioadmin

spring:
  cloud:
    gateway:
      routes:
        - id: s3-route
          uri: ${s3.endpoint}
          predicates:
            - Path=/s3/**
          filters:
            - PreserveHostHeader
            - StripPrefix=1
            - AWSSign=${s3.endpoint},${s3.ak},${s3.sk}

调用成功:
在这里插入图片描述
基于上面的验证,后续其实就可以实现标准的S3协议,同时也可以很方便的对S3进行扩展,比如限流限速,对S3用户权限进行扩展等等能力。

结语

ChatGPT真的很厉害,它确实可以帮助我们解决很多问题。

代码地址:https://github.com/yzh19961031/blogDemo/tree/master/s3-gateway

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值