RestTemplate无法使用S3预先签名的放置网址

时间:2017-11-11 14:38:09

标签: java spring amazon-s3 resttemplate

我有一台服务器生成AWS S3预先签名的PUT网址,然后我尝试使用RestTemplate将byte[]上传到该网址,并使用以下代码:

RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.ALL));
HttpEntity<byte[]> entity = new HttpEntity<>("Testing testing testing".getBytes(), headers);
System.out.println(restTemplate.exchange(putUrl, HttpMethod.PUT, entity, String.class));

当我运行该代码时,我收到此错误:

Exception in thread "JavaFX Application Thread" org.springframework.web.client.HttpClientErrorException: 400 Bad Request
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:63)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:531)
    at tech.dashman.dashman.controllers.RendererAppController.lambda$null$2(RendererAppController.java:95)

不幸的是,AWS S3日志中没有任何内容,因此,我不确定发生了什么。如果我使用完全相同的URL并将其放在IntelliJ IDEA的REST客户端中,它就可以工作(它在S3中创建一个空文件)。

任何想法我的Java代码有什么问题?

以下是完成签名并尝试将小型有效负载上传到S3的完整示例:

import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import org.joda.time.DateTime;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.web.client.RestTemplate;
import java.util.Date;

public class S3PutIssue {
    static public void main(String[] args) {
        String awsAccessKeyId = "";
        String awsSecretKey = "";
        String awsRegion = "";
        String path = "";
        String awsBucketName = "";
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(awsAccessKeyId, awsSecretKey);
        AmazonS3 s3Client = AmazonS3ClientBuilder.standard().withRegion(awsRegion).
                withCredentials(new AWSStaticCredentialsProvider(awsCredentials)).build();
        Date expiration = new DateTime().plusDays(1).toDate();
        GeneratePresignedUrlRequest urlRequest = new GeneratePresignedUrlRequest(awsBucketName, path);
        urlRequest.setMethod(HttpMethod.PUT);
        urlRequest.setExpiration(expiration);
        String putUrl = s3Client.generatePresignedUrl(urlRequest).toString();

        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        HttpEntity<byte[]> entity = new HttpEntity<>("Testing testing testing".getBytes(), headers);
        restTemplate.exchange(putUrl, org.springframework.http.HttpMethod.PUT, entity, Void.class);
    }
}

2 个答案:

答案 0 :(得分:2)

请勿将您的网址转换为字符串。而是将其转换为URI。我认为转换为String时存在一些编码问题。例如,字符串格式的网址为%252F,其中应该只有%2F。看起来像某种双重编码问题。

保留为URL ...

     URL putUrl = amazonS3Client.generatePresignedUrl(urlRequest);

转换为URI ...

    ResponseEntity<String> re = restTemplate.exchange(putUrl.toURI(), org.springframework.http.HttpMethod.PUT, entity, String.class);

编辑:更多信息,以澄清发生了什么。

此处发生的问题是,在此实例中调用URL.toString()时,会返回URL的编码字符串表示形式。但RestTemplate期待一个尚未编码的String url。 RestTemplate将为您进行编码。

例如,请查看以下代码......

public static void main(String[] args) {
    RestTemplate rt = new RestTemplate();
    rt.exchange("http://foo.com/?var=<val>", HttpMethod.GET, HttpEntity.EMPTY, String.class);
}

当你运行它时,你会从Spring获得以下调试消息,注意调试消息中的url是如何编码的。

[main] DEBUG org.springframework.web.client.RestTemplate - Created GET request for "http://foo.com/?var=%3Cval%3E" 

因此,您可以看到RestTemplate将为您编码传递给它的任何String url。但是AmazonS3Client提供的URL已经编码。请参阅下面的代码。

URL putUrl = amazonS3Client.generatePresignedUrl(urlRequest);
System.out.println("putUrl.toString = " + putUrl.toString());

这将打印出已编码的字符串。

https://private.s3.amazonaws.com/testing/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20171114T191829Z&X-Amz-SignedHeaders=host&X-Amz-Expires=0&X-Amz-Credential=AKIAIJ7ZSL22IJTM6NTQ%2F20171114%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=eca611ea33d9ad5710207568dcf181e4318ce39271fd0f1ce05bd99ebbf4097

因此,当我将其粘贴到RestTemplate的交换方法中时,我得到以下调试消息。

[main] DEBUG org.springframework.web.client.RestTemplate - PUT request for "https://turretmaster.s3.amazonaws.com/testing/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20171114T191829Z&X-Amz-SignedHeaders=host&X-Amz-Expires=0&X-Amz-Credential=AKIAIJ7ZSL22IJTM6NTQ%252F20171114%252Fus-east-1%252Fs3%252Faws4_request&X-Amz-Signature=eca611ea33d9ad5710207568dcf181e4318ce39271fd0f1ce05bd99ebbf40975"

注意url String中的每个%2F如何变成%252F%2F/的编码表示。但%25%。所以它编码了一个已编码的网址。解决方案是将URI对象传递给RestTemplate.exchange,而不是编码的String url。

答案 1 :(得分:2)

问题的根源是网址字符的双重编码。扩展密钥中有/%2编码为s3Client.generatePresignedUrl。当已编码的字符串传递给restTemplate.exchange时,它会在内部转换为URI,并在%252源代码中由UriTemplateHandlerRestTemplate次编码。

@Override
@Nullable
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {

    URI expanded = getUriTemplateHandler().expand(url, uriVariables);
    return doExecute(expanded, method, requestCallback, responseExtractor);
}

所以最简单的解决方案是使用URL.toURI()将URL转换为URI。如果您在调用URI时没有String并且RestTemplate,则可能有两个选项。

将字符串传递给字符串以交换方法。

restTemplate.exchange(new URI(putUrl.toString()), HttpMethod.PUT, entity, Void.class);

使用UriTemplateHandler编码模式创建默认NONE并将其传递给RestTemplate

DefaultUriBuilderFactory defaultUriBuilderFactory = new DefaultUriBuilderFactory();
defaultUriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
restTemplate.setUriTemplateHandler(defaultUriBuilderFactory);
restTemplate.exchange(putUrl.toString(), org.springframework.http.HttpMethod.PUT, entity, Void.class);