2013-03-18 1 views
5

Я использую SDK Java Amazon для работы с S3 для хранения загруженных файлов. Я хотел бы сохранить исходное имя файла, и я помещаю его в конец ключа, но я также использую структуру виртуального каталога - что-то вроде <dirname>/<uuid>/<originalFilename>.Amazon S3 Собственные URL-адреса, удаляющие косые черты в ключе

Проблема заключается в том, что, когда я хочу, чтобы создать presigned URL для загрузки с помощью API, как:

URL url = s3Client.generatePresignedUrl(generatePresignedUrlRequest); 
return url.toExternalForm(); 

В SDK URL ускользает весь ключ, в том числе косой черты. Хотя он все еще работает, это означает, что имя загруженного файла включает в себя весь ключ, а не только оригинальный бит имени файла в конце. Я знаю, что это должно быть возможно сделать, не ускользая от косых черт, но я стараюсь не переписывать много кода уже в SDK. Есть ли общее решение для этого? Я знаю, что я использовал веб-приложения, которые следуют одному и тому же шаблону и не имеют проблемы слэш-сбой.

+0

Если у ведра есть ACL, который разрешает анонимный доступ, вы можете получить файл по следующему шаблону: //s3.amazonaws.com/ /<полное имя ключа с косой чертой>. Это то, что вы так же ищете? –

+0

@JasonSperske Он находится в частном ведре. –

ответ

1

Я все еще надеюсь на лучшее решение, чем это, но, увидев, что @aKzenT подтвердил мой вывод о том, что для этого не существует существующего решения, я написал один. Это просто простой подкласс AmazonS3Client. Я беспокоюсь, что это хрупкое, потому что мне пришлось копировать много кода из метода, который я перегружал, но он кажется самым минимальным решением. Я могу подтвердить, что он отлично работает в моей собственной базе кода. Я отправил код в gist, но ради полного ответа:

import com.amazonaws.AmazonWebServiceRequest; 
import com.amazonaws.HttpMethod; 
import com.amazonaws.Request; 
import com.amazonaws.auth.AWSCredentials; 
import com.amazonaws.handlers.RequestHandler; 
import com.amazonaws.services.s3.AmazonS3Client; 
import com.amazonaws.services.s3.Headers; 
import com.amazonaws.services.s3.internal.S3QueryStringSigner; 
import com.amazonaws.services.s3.internal.ServiceUtils; 

import java.util.Date; 

/** 
* This class should be a drop in replacement for AmazonS3Client as long as you use the single credential 
* constructor. It could probably be modified to add additional constructors if needed, but this is the one we use. 
* Supporting all of them didn't seem trivial because of some dependencies in the original presignRequest method. 
* 
* The only real purpose of this class is to change the behavior of generating presigned URLs. The original version 
* escaped slashes in the key and this one does not. Pretty url paths are kept intact. 
* 
* @author Russell Leggett 
*/ 
public class PrettyUrlS3Client extends AmazonS3Client{ 
    private AWSCredentials awsCredentials; 

    /** 
    * This constructor is the only one provided because it is only one I needed, and it 
    * retains awsCredentials which might be needed in the presignRequest method 
    * 
    * @param awsCredentials 
    */ 
    public PrettyUrlS3Client(AWSCredentials awsCredentials) { 
     super(awsCredentials); 
     this.awsCredentials = awsCredentials; 
    } 

    /** 
    * WARNING: This method is an override of the AmazonS3Client presignRequest 
    * and copies most of the code. Should be careful of updates to the original. 
    * 
    * @param request 
    * @param methodName 
    * @param bucketName 
    * @param key 
    * @param expiration 
    * @param subResource 
    * @param <T> 
    */ 
    @Override 
    protected <T> void presignRequest(Request<T> request, HttpMethod methodName, String bucketName, String key, Date expiration, String subResource) { 

     // Run any additional request handlers if present 
     if (requestHandlers != null) { 
      for (RequestHandler requestHandler : requestHandlers) { 
       requestHandler.beforeRequest(request); 
      } 
     } 
     String resourcePath = "/" + 
       ((bucketName != null) ? bucketName + "/" : "") + 
       ((key != null) ? keyToEscapedPath(key)/* CHANGED: this is the primary change */ : "") + 
       ((subResource != null) ? "?" + subResource : ""); 

     //the request apparently needs the resource path without a starting '/' 
     request.setResourcePath(resourcePath.substring(1));//CHANGED: needed to match the signature with the URL generated from the request 
     AWSCredentials credentials = awsCredentials; 
     AmazonWebServiceRequest originalRequest = request.getOriginalRequest(); 
     if (originalRequest != null && originalRequest.getRequestCredentials() != null) { 
      credentials = originalRequest.getRequestCredentials(); 
     } 

     new S3QueryStringSigner<T>(methodName.toString(), resourcePath, expiration).sign(request, credentials); 

     // The Amazon S3 DevPay token header is a special exception and can be safely moved 
     // from the request's headers into the query string to ensure that it travels along 
     // with the pre-signed URL when it's sent back to Amazon S3. 
     if (request.getHeaders().containsKey(Headers.SECURITY_TOKEN)) { 
      String value = request.getHeaders().get(Headers.SECURITY_TOKEN); 
      request.addParameter(Headers.SECURITY_TOKEN, value); 
      request.getHeaders().remove(Headers.SECURITY_TOKEN); 
     } 
    } 

    /** 
    * A simple utility method which url escapes an S3 key, but leaves the 
    * slashes (/) unescaped so they can stay part of the url. 
    * @param key 
    * @return 
    */ 
    public static String keyToEscapedPath(String key){ 
     String[] keyParts = key.split("/"); 
     StringBuilder result = new StringBuilder(); 
     for(String part : keyParts){ 
      if(result.length()>0){ 
       result.append("/"); 
      } 
      result.append(ServiceUtils.urlEncode(part)); 
     } 
     return result.toString().replaceAll("%7E","~"); 
    } 
} 

UPDATE Я обновил суть и этот код, чтобы исправить проблему, у меня был с ~ 's. Это происходило даже при использовании стандартного клиента, но не выполнялось закрепление его. См. Gist для получения дополнительной информации/отслеживания любых дальнейших изменений, которые я мог бы сделать.

7

Это ошибка в текущем Java SDK:

Если посмотреть на https://github.com/aws/aws-sdk-java/blob/master/src/main/java/com/amazonaws/services/s3/AmazonS3Client.java#L2820

Метод presignRequest, который вызывается внутри имеет следующий код:

String resourcePath = "/" + 
     ((bucketName != null) ? bucketName + "/" : "") + 
     ((key != null) ? ServiceUtils.urlEncode(key) : "") + 
     ((subResource != null) ? "?" + subResource : ""); 

Ключ URL закодированный здесь перед подписанием, который, я думаю, является ошибкой.

Возможно, вы сможете унаследовать от AmazonS3Client и переопределить функцию, чтобы исправить это.

В некоторых местах предлагается использовать url.getQuery() и прикрепить его оригинальным awsURL (https://forums.aws.amazon.com/thread.jspa?messageID=356271). Однако, как вы сказали, это приведет к ошибке, потому что ключ ресурса не будет соответствовать сигнатуре.

Следующая проблема также может быть связано, я не проверить предложенную workarround:

How to generate pre-signed Amazon S3 url for a vanity domain, using amazon sdk?

Amazon признали и исправили подобную ошибку раньше: https://forums.aws.amazon.com/thread.jspa?messageID=418537

Так что я надеюсь, что это будет быть исправлена ​​в следующей версии.

+0

Вы пробовали это? Я сделал что-то подобное и получил подпись 403, не соответствует. То же самое было описано на этом форуме. «К сожалению, не совсем правильно говорить, что вы можете сделать это в обоих направлениях. Это нормально, если вы используете URL-адрес, сгенерированный Java SDK, в общем виде. К сожалению, если вы передадите этот URL-адрес .NET-приложению, использует стандартный класс WebRequest для использования URL-адреса, .NET декодирует% 2F в /, а затем запрос с ошибкой 403 - подпись не соответствует. " –

+0

Как я понял, ваше сообщение работает, если вы вручную используете слайсы, но вы не хотите писать код, чтобы сделать это, нет? Использование url.getQuery должно делать именно это. – aKzenT

+0

Вы не можете смешивать и сопоставлять. Когда вызывается generatePresignedUrl, он берет весь ключ и ускользает от него, а затем создает подпись с помощью экранированного ключа. Вы не можете использовать подпись из экранированного ключа и объединить ее с неэкранированным ключом в URL. Гипотетически, если вы могли бы выполнить код подписи без экранирования, вы могли бы использовать ключ без экранирования. Мне просто интересно, есть ли у кого-то простой способ сделать это. –

1

версия 1.4.3 Java SDK, похоже, устранила эту проблему. Возможно, это было исправлено ранее, но я могу подтвердить, что он правильно работает в 1.4.3.

Смежные вопросы