【问题标题】:Amazon S3 Presigned URLs escape the slashes in the keyAmazon S3 预签名 URL 转义密钥中的斜杠
【发布时间】:2013-03-18 09:38:42
【问题描述】:

我正在使用 Java Amazon SDK 与 S3 一起使用来存储上传的文件。我想保留原始文件名,并将其放在密钥的末尾,但我也在使用虚拟目录结构 - 类似于<dirname>/<uuid>/<originalFilename>

问题是,当我想使用 api 生成预签名 URL 进行下载时:

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

sdk url 转义了整个密钥,包括斜杠。虽然它仍然有效,但这意味着下载的文件的名称包括整个密钥,而不是最后的原始文件名位。我知道在不转义斜杠的情况下应该可以做到这一点,但我试图避免重写 SDK 中已有的大量代码。有一个通用的解决方案吗?我知道我使用的网络应用程序遵循相同的模式并且没有斜线转义问题。

【问题讨论】:

  • 如果存储桶具有允许匿名访问的 ACL,那么您可以通过以下模式检索文件://s3.amazonaws.com//。这就是你要找的吗?
  • @JasonSperske 它在一个私有存储桶中。

标签: java amazon-s3 urlencode


【解决方案1】:

这是当前 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)。但是,正如您自己所说,这会产生错误,因为资源密钥与签名不匹配。

以下问题也可能与此有关,我没有查看建议的解决方法:

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

亚马逊之前发现并修复了一个类似的错误: https://forums.aws.amazon.com/thread.jspa?messageID=418537

所以我希望它会在下一个版本中修复。

【讨论】:

  • 你试过了吗?我做了类似的事情,得到一个 403 签名不匹配。该论坛帖子中也描述了相同的内容。 “不幸的是,说你可以同时做到这一点并不完全正确。如果你以通用方式使用 Java SDK 生成的 URL,那很好。不幸的是,如果你将该 URL 交给一个 .NET 应用程序并且它使用它使用 URL 的标准 WebRequest 类,.NET 将 %2F 解码为 /,然后请求失败并返回 403 - 签名不匹配。”
  • 据我了解,如果您手动使用斜线,它可以工作,但您不想编写代码来执行此操作,不是吗?使用 url.getQuery 应该可以做到这一点。
  • 不能混搭。当调用 generatePresignedUrl 时,它会获取整个密钥并将其转义,然后使用转义的密钥创建签名。您不能使用转义密钥中的签名并将其与 URL 中的未转义密钥结合使用。假设,如果您可以在不转义的情况下执行签名代码,那么您可以在不转义的情况下使用密钥。我只是想知道是否有人有一个简单的方法来做到这一点。
  • 嘿,对不起。我反应太快了。做更多的研究并查看代码,我得出了相同的结论。查看我最近的编辑。长话短说,我认为最简单的方法是覆盖错误的方法,或者通过查看亚马逊内部如何实现您自己的版本。
  • 是的,我得出了同样的结论,并在一个令人沮丧的不方便的地方浏览了代码。这就是我来这里的原因 :) 认为有人可能已经这样做了。
【解决方案2】:

我仍然希望有一个比这更好的解决方案,但是看到@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","~");
    }
}

更新我更新了要点和这段代码来解决我在使用 ~ 时遇到的问题。即使使用标准客户端也会发生这种情况,但是取消转义 ~ 修复了它。请参阅要点了解更多详细信息/跟踪我可能做出的任何进一步更改。

【讨论】:

    【解决方案3】:

    Java SDK 1.4.3 版似乎已经解决了这个问题。也许,它早先已修复,但我可以确认它在 1.4.3 中正常工作。

    【讨论】:

      猜你喜欢
      • 2018-12-27
      • 2021-10-18
      • 1970-01-01
      • 1970-01-01
      • 2021-12-26
      • 1970-01-01
      • 2020-07-30
      • 2018-12-26
      • 2013-03-20
      相关资源
      最近更新 更多