2017-11-11 165 views
1

我有一台服务器产生AWS S3前签署PUT网址的工作,然后我试图上传到byte[]使用RestTemplate这个代码,网址:RestTemplate不与S3预签把网址

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); 
    } 
} 
+0

从你所描述的我可以想象,你的代码不会产生与IDEA的REST客户端一样的调用。我可以推荐设置日志级别,这样你可以看到底层的HTTP相关日志,看看究竟发送了什么 - 我期望它的标题有些不同。另外,如果你不使用PUT的签名,你的代码是否工作? – Xonix

+0

@Xonix:如何增加RestTemplate的日志输出?我认为所提出的要求几乎可以肯定,但我找不到差别。 – Pablo

+0

你是什么意思的“预签名”网址? –

回答

1

问题的根源是url字符的双重编码。扩展密钥中有/,其编码为%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并且在调用RestTemplate时有String,则有两种选择是可能的。

通过URI来代替字符串来交换方法。

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

创建默认UriTemplateHandlerNONE编码模式,并把它传递给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); 
2

您的网址,而不要转换为字符串。而是将其转换为URI。我认为在转换为字符串时存在一些编码问题。例如,String格式的URL有%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); 
} 

当你运行这个你从春天得到以下调试消息,请注意在调试味精的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" 

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

+0

这绝对是一个很好的发现,但putUrl必须是字符串。它在服务器上生成并放入数据库,然后作为JSON发送给执行实际PUT的客户端。 – Pablo

+0

如果我打印这个url字符串,将它复制并粘贴到IntelliJ的REST Client中,它可以工作,所以我认为它不会被破坏。 – Pablo

+0

你试过了吗?我告诉你,我复制并粘贴了你的代码,重现了这个问题,然后做了我提到的改变,问题就解决了。当我从RestTemplate调试(而不是postUrl字符串)复制并粘贴String url时,它有双重编码。这就是我在Chrome上使用Postman重现的String url。 –