2017-11-11 84 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。 –