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-routeuri: s3-endpiontpredicates:- 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-routeuri: s3-endpiontpredicates:- 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);}@Overridepublic 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;}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("endpoint", "ak", "sk");}@Data@AllArgsConstructor@NoArgsConstructorpublic static class Config {private String endpoint;private String ak;private String sk;}
}
配置文件中生效过滤器:
s3:endpoint: http://10.1.140.3:9000ak: minioadminsk: minioadminspring:cloud:gateway:routes:- id: s3-routeuri: ${s3.endpoint}predicates:- Path=/**filters:- PreserveHostHeader- StripPrefix=1- AWSSign=${s3.endpoint},${s3.ak},${s3.sk}
调用成功:
基于上面的验证,后续其实就可以实现标准的S3协议,同时也可以很方便的对S3进行扩展,比如限流限速,对S3用户权限进行扩展等等能力。
结语
ChatGPT真的很厉害,它确实可以帮助我们解决很多问题。
代码地址:https://github.com/yzh19961031/blogDemo/tree/master/s3-gateway