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=/**
filters:
- PreserveHostHeader
- StripPrefix=1
- AWSSign=${s3.endpoint},${s3.ak},${s3.sk}
调用成功:
基于上面的验证,后续其实就可以实现标准的S3协议,同时也可以很方便的对S3进行扩展,比如限流限速,对S3用户权限进行扩展等等能力。
结语
ChatGPT真的很厉害,它确实可以帮助我们解决很多问题。
代码地址:https://github.com/yzh19961031/blogDemo/tree/master/s3-gateway