spring-boot - S3AsyncClient - 删除授权标头
问题描述
我正在尝试使用 S3AsyncClient 下载具有预签名 URL 的 S3 对象。但是,请求仍然带有 Authorization 标头,包括使用与此特定 S3 实例不兼容的密码生成的签名(这是模拟 AWS S3 对象存储的 Enterprise S3 实例)。这会导致 SSLException。当我在浏览器中或通过没有授权标头的 Postman 发送请求时,该 URL 有效。
我可以通过 S3AsyncClient 发送这种带有预签名 URL 的 GET 请求,而无需授权、x-amz-content-sha256 和 X-Amz-Date 标头吗?
登录预签名 URL:
Generating pre-signed URL.
Pre-Signed URL: https://<namespace>.<endpoint>/<bucket>/<key>?AWSAccessKeyId=<access-id>01&Expires=1600801209&Signature=......../..................=
HTTP 请求的调试日志如下所示(我会注意到预签名 URL 中的 Signature 参数现在已编码并放置在其他参数之前,尽管当我提取 URL 并发送 GET 请求时这似乎无关紧要):
GET /<bucket>/VADRUserGuide.docx?Signature=........%2F..................%3D&AWSAccessKeyId=<access-id>&Expires=1600801209 HTTP/1.1
Host: <namespace>.<endpoint>
amz-sdk-invocation-id: ........-....-....-....-............
amz-sdk-retry: 3/152/440
Authorization: AWS4-HMAC-SHA256 Credential=access-id>/20200922/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-retry;authorization;host;x-amz-content-sha256;x-amz-date, Signature=.................................................
User-Agent: aws-sdk-java/2.10.86 Windows_10/10.0 Java_HotSpot_TM__64-Bit_Server_VM/25.191-b12 Java/1.8.0_191 vendor/Oracle_Corporation io/async http/UNKNOWN
x-amz-content-sha256: UNSIGNED-PAYLOAD
X-Amz-Date: 20200922T213232Z)
/**
* @author Philippe
*
*/
@RestController
@RequestMapping("/inbox")
@Slf4j
public class DownloadResource {
private final GeneratePresignedURL generatePresignedURL;
private final S3AsyncClient s3client;
private final S3ClientConfigurationProperties s3config;
public DownloadResource(S3AsyncClient s3client, S3ClientConfigurationProperties s3config, GeneratePresignedURL generatePresignedURL) {
this.s3client = s3client;
this.s3config = s3config;
this.generatePresignedURL = generatePresignedURL;
}
@GetMapping(path="/{filekey}")
public Mono<ResponseEntity<Flux<ByteBuffer>>> downloadFile(@PathVariable("filekey") String filekey) {
//generate pre-signed URL
final String url = generatePresignedURL.getPresignedUrl(filekey);
//extract signed URL params
final String pattern = "(\\?|\\&)([^=]+)\\=([^&]+)";
List<String> params = new ArrayList<>();
Matcher m = Pattern.compile(pattern)
.matcher(url);
while (m.find()) {
params.add(m.group());
}
final String[] p0 = params.get(0).substring(1).split("=",2);
final String[] p1 = params.get(1).substring(1).split("=",2);
final String[] p2 = params.get(2).substring(1).split("=",2);
//attach signed URL params via AwsRequestOverrideConfiguration
AwsRequestOverrideConfiguration overrideConfiguration = AwsRequestOverrideConfiguration.builder()
.putRawQueryParameter(p0[0], p0[1])
.putRawQueryParameter(p1[0], p1[1])
.putRawQueryParameter(p2[0], p2[1])
.build();
GetObjectRequest request = GetObjectRequest.builder()
.key(filekey)
.overrideConfiguration(overrideConfiguration)
.bucket(s3config.getBucket())
.build();
return Mono.fromFuture(s3client.getObject(request,new FluxResponseProvider()))
.map( (response) -> {
checkResult(response.sdkResponse);
String filename = getMetadataItem(response.sdkResponse,"filename",filekey);
log.info("[I65] filename={}, length={}",filename, response.sdkResponse.contentLength() );
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, response.sdkResponse.contentType())
.header(HttpHeaders.CONTENT_LENGTH, Long.toString(response.sdkResponse.contentLength()))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.body(response.flux);
});
}
private void printRequestFields(GetObjectRequest request){
System.out.println("request fields:");
String[] parValues = new String[6];
Field[] fields = request.getClass().getDeclaredFields();
//print field names paired with their values
for ( Field field : fields ) {
try {
field.setAccessible(true);
System.out.println( field.getName() +": ");
//requires access to private field:
System.out.println( field.get(request) );
} catch ( IllegalAccessException ex ) {
System.out.println(ex);
}
}
}
/**
* Lookup a metadata key in a case-insensitive way.
* @param sdkResponse
* @param key
* @param defaultValue
* @return
*/
private String getMetadataItem(GetObjectResponse sdkResponse, String key, String defaultValue) {
for( Entry<String, String> entry : sdkResponse.metadata().entrySet()) {
if ( entry.getKey().equalsIgnoreCase(key)) {
return entry.getValue();
}
}
return defaultValue;
}
// Helper used to check return codes from an API call
private static void checkResult(GetObjectResponse response) {
SdkHttpResponse sdkResponse = response.sdkHttpResponse();
if ( sdkResponse != null && sdkResponse.isSuccessful()) {
return;
}
throw new DownloadFailedException(response);
}
static class FluxResponseProvider implements AsyncResponseTransformer<GetObjectResponse,FluxResponse> {
private FluxResponse response;
@Override
public CompletableFuture<FluxResponse> prepare() {
response = new FluxResponse();
return response.cf;
}
@Override
public void onResponse(GetObjectResponse sdkResponse) {
this.response.sdkResponse = sdkResponse;
}
@Override
public void onStream(SdkPublisher<ByteBuffer> publisher) {
response.flux = Flux.from(publisher);
response.cf.complete(response);
}
@Override
public void exceptionOccurred(Throwable error) {
response.cf.completeExceptionally(error);
}
}
/**
* Holds the API response and stream
* @author Philippe
*/
static class FluxResponse {
final CompletableFuture<FluxResponse> cf = new CompletableFuture<>();
GetObjectResponse sdkResponse;
Flux<ByteBuffer> flux;
}
}
S3AsyncClient 的配置(如您所见,凭据提供程序已被注释掉):
@Configuration
@EnableConfigurationProperties(S3ClientConfigurationProperties.class)
public class S3ClientConfiguration {
@Bean
public S3AsyncClient s3client(S3ClientConfigurationProperties s3props) {
SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder()
.writeTimeout(Duration.ZERO)
.maxConcurrency(64)
.build();
S3Configuration serviceConfiguration = S3Configuration.builder()
.checksumValidationEnabled(false)
.chunkedEncodingEnabled(true)
.build();
S3AsyncClientBuilder b = S3AsyncClient.builder()
.httpClient(httpClient)
.region(s3props.getRegion())
// credentials provider commented out
.serviceConfiguration(serviceConfiguration);
if (s3props.getEndpoint() != null) {
b = b.endpointOverride(s3props.getEndpoint());
}
return b.build();
}
// credentials provider Bean commented out
}
常规 AmazonS3 客户端的客户端(用于 GeneratePresignedUrlRequest):
@Configuration
public class S3Configuration {
@Bean
public S3Storage s3Storage(S3ServiceInfo s3ServiceInfo) {
final ClientConfiguration httpsClientConfig = new ClientConfiguration().withProtocol(Protocol.HTTPS).withSignerOverride("S3SignerType");
SSLContext sslContext = SSLContexts.createSystemDefault();
httpsClientConfig.getApacheHttpClientConfig().setSslSocketFactory(new SSLConnectionSocketFactory(sslContext));
AmazonS3 client = AmazonS3ClientBuilder.standard()
.withPathStyleAccessEnabled(true)
.withForceGlobalBucketAccessEnabled(true)
.withClientConfiguration(httpsClientConfig)
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(s3ServiceInfo.getAccessKey(), s3ServiceInfo.getSecretKey())))
.withEndpointConfiguration(new EndpointConfiguration(endpointUrlWithNamespace(s3ServiceInfo.getEndpoint(), s3ServiceInfo.getAccessKey()), null))
.build();
return new S3Storage(client, s3ServiceInfo.getBucket(), s3ServiceInfo.getEndpoint());
}
protected String endpointUrlWithNamespace(String endpoint, String accessKey) {
//extract namespace from access-key (verify format)
Matcher matcher = Pattern.compile("((.+?-){3}ns\\d\\d)-").matcher(accessKey);
if (!matcher.find()) return endpoint;
String namespace = matcher.group(1);
return endpoint.contains("://s3") ? endpoint.replace("://", "://" + namespace + ".") : endpoint;
}
@Configuration
public class S3LocalConfiguration {
@Bean
public S3ServiceInfo s3ServiceInfo(
@Value("${s3.accessKey}") String accessKey,
@Value("${s3.secretKey}") String secretKey,
@Value("${s3.bucket}") String bucket,
@Value("${s3.endpoint}") String endpoint) {
return new S3ServiceInfo(null, accessKey, secretKey, endpoint, bucket);
}
}
@Data
@AllArgsConstructor
public static class S3Storage {
private AmazonS3 client;
private String bucket;
private String endpoint;
}
}
解决方案
推荐阅读
- django - Django 内置 url 'logout' 反向 URL 正在返回一个相对路径
- javascript - 在 Pug 中循环多个变量
- html - CountryDropdown 看起来类似于默认的 Select 样式
- c# - Console.Clear() 没有清除控制台
- c# - C# Asp.Netcore31:无法从程序集“Microsoft.AspNetCore.Mvc.Formatters.Json”加载类型“Microsoft.AspNetCore.Mvc.Formatters.JsonInputFormatter”
- azure-devops - 在 DevOps 管道中使用 ARM TTK 工具测试 ARM 模板
- java - 为什么HashSet在依赖默认hash和equals时有时不添加对象?
- python - 制作烟花
- function - 截至 2021 年,所有常见的 lisp 特殊功能都有哪些?
- python - (不平衡)面板或合并数据?