php AWS S3预签名请求缓存

nkkqxpd9  于 2023-10-15  发布在  PHP
关注(0)|答案(6)|浏览(116)

我想将用户个人资料图片存储在S3存储桶中,但要保持这些图片的私密性。为了做到这一点,我创建了一个预签名的网址,每当图像是必需的。然而,这每次都会创建一个唯一的URL,这意味着浏览器永远不会缓存图像,我最终会在GET请求中付出更多。
下面是我的代码生成的URL的例子,我使用Laravel:

  1. $s3 = \Storage::disk('s3');
  2. $client = $s3->getDriver()->getAdapter()->getClient();
  3. $expiry = new \DateTime('2017-07-25');
  4. $command = $client->getCommand('GetObject', [
  5. 'Bucket' => \Config::get('filesystems.disks.s3.bucket'),
  6. 'Key' => $key
  7. ]);
  8. $request = $client->createPresignedRequest($command, $expiry);
  9. return (string) $request->getUri();

我认为通过指定日期时间而不是时间单位,它会创建相同的URL,但实际上它会将剩余的秒数添加到URL,这里有一个例子:
xxxx.s3.eu-west-2.amazonaws.com/profile-pics/92323.png?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AXXXXXXXXXXX%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20170720T112123Z&X-Amz-SignedHeaders=host&X-Amz-Expires=391117&X-Amz-Signature=XXXXXXXXX

是否可以生成可重复的预签名请求URL,以便用户浏览器可以缓存图像?

hmtdttj4

hmtdttj41#

这是我在看完这篇文章后想到的一个python解决方案。它使用freezegun库来操纵时间,使签名在给定的时间段内保持不变。

  1. import time
  2. import datetime
  3. import boto3
  4. from freezegun import freezetime
  5. S3_CLIENT = boto3.client("s3")
  6. SEVEN_DAYS_IN_SECONDS = 604800
  7. MAX_EXPIRES_SECONDS = SEVEN_DAYS_IN_SECONDS
  8. def get_presigned_get_url(bucket: str, key: str, expires_in_seconds: int = MAX_EXPIRES_SECONDS) -> str:
  9. current_timestamp = int(time.time())
  10. truncated_timestamp = current_timestamp - (current_timestamp % expires_in_seconds)
  11. with freeze_time(datetime.datetime.fromtimestamp(truncated_timestamp)):
  12. presigned_url = S3_CLIENT.generate_presigned_url(
  13. ClientMethod="get_object",
  14. Params={
  15. "Bucket": bucket,
  16. "Key": key,
  17. "ResponseCacheControl": f"private, max-age={expires_in_seconds}, immutable",
  18. },
  19. ExpiresIn=expires_in_seconds,
  20. HttpMethod="GET",
  21. )
  22. return presigned_url
展开查看全部
hc8w905p

hc8w905p2#

也许是一个迟来的回复,但我会添加我的方法,以利于人们阅读这在未来。
为了强制浏览器缓存生效,每次都生成相同的URL是很重要的,直到你特别希望浏览器从服务器重新加载内容。不幸的是,sdk中提供的预签名者依赖于当前的时间戳,每次都会导致一个新的url。
这个例子是用Java编写的,但是它可以很容易地扩展到其他语言。
GetObjectRequest构建器(用于创建预签名的URL)允许覆盖配置。我们可以提供一个自定义签名器来修改它的行为

  1. AwsRequestOverrideConfiguration.builder()
  2. .signer(new CustomAwsS3V4Signer())
  3. .credentialsProvider(<You may need to provide a custom credential provider
  4. here>)))
  5. .build())
  6. GetObjectRequest getObjectRequest =
  7. GetObjectRequest.builder()
  8. .bucket(getUserBucket())
  9. .key(key)
  10. .responseCacheControl("max-age="+(TimeUnit.DAYS.toSeconds(7)+ defaultIfNull(version,0L)))
  11. .overrideConfiguration(overrideConfig)
  12. .build();
  13. public class CustomAwsS3V4Signer implements Presigner, Signer
  14. {
  15. private final AwsS3V4Signer awsSigner;
  16. public CustomAwsS3V4Signer()
  17. {
  18. awsSigner = AwsS3V4Signer.create();
  19. }
  20. @Override
  21. public SdkHttpFullRequest presign(SdkHttpFullRequest request, ExecutionAttributes executionAttributes)
  22. {
  23. Instant baselineInstant = Instant.now().truncatedTo(ChronoUnit.DAYS);
  24. executionAttributes.putAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION,
  25. baselineInstant.plus(3, ChronoUnit.DAYS));

在这里,我们覆盖签名时钟来模拟一个固定的时间,这最终会导致url中的过期和签名一致,直到未来的某个日期:

  1. Aws4PresignerParams.Builder builder = Aws4PresignerParams.builder()
  2. .signingClockOverride(Clock.fixed(baselineInstant, ZoneId.of("UTC")));
  3. Aws4PresignerParams signingParams =
  4. extractPresignerParams(builder, executionAttributes).build();
  5. return awsSigner.presign(request, signingParams);
  6. }
  7. }

更多详情请点击此处:
https://murf.ai/resources/creating-cache-friendly-presigned-s3-urls-using-v4signer-q1bbqgk

展开查看全部
vcirk6k6

vcirk6k63#

与其使用预签名的URL机制,不如向应用程序添加一个经过身份验证的端点,并在该端点内检索图像?在你的img标签中使用这个URL。这个端点可以缓存图像,并为浏览器提供适当的响应头来缓存图像。

t5fffqht

t5fffqht4#

类似于@Aragorn的概念,但这是更完整的代码。这又是Java。此外,由于我的应用程序是多区域的,我不得不把在区域属性。

  1. import lombok.SneakyThrows;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.beans.factory.annotation.Value;
  5. import org.springframework.stereotype.Component;
  6. import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
  7. import software.amazon.awssdk.core.signer.Signer;
  8. import software.amazon.awssdk.services.s3.S3AsyncClient;
  9. import software.amazon.awssdk.services.s3.S3Client;
  10. import software.amazon.awssdk.services.s3.model.GetObjectRequest;
  11. import software.amazon.awssdk.services.s3.model.PutObjectRequest;
  12. import software.amazon.awssdk.services.s3.presigner.S3Presigner;
  13. import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
  14. import javax.annotation.PostConstruct;
  15. import javax.validation.constraints.NotNull;
  16. import java.net.URI;
  17. import java.net.URISyntaxException;
  18. import java.nio.file.Path;
  19. import java.time.Duration;
  20. import java.util.UUID;
  21. import java.util.concurrent.CompletableFuture;
  22. @Component
  23. @Slf4j
  24. public class S3Operations {
  25. @Autowired
  26. private Signer awsSigner;
  27. private final Map<Region, S3Presigner> presignerMap = new ConcurrentHashMap<>();
  28. private S3Presigner buildPresignerForRegion(
  29. AwsCredentialsProvider credentialsProvider,
  30. Region region) {
  31. return S3Presigner.builder()
  32. .credentialsProvider(credentialsProvider)
  33. .region(region)
  34. .build();
  35. }
  36. /**
  37. * Convert an S3 URI to a normal HTTPS URI that expires.
  38. *
  39. * @param s3Uri S3 URI (e.g. s3://bucketname/ArchieTest/フェニックス.jpg)
  40. * @return https URI
  41. */
  42. @SneakyThrows
  43. public URI getExpiringUri(final URI s3Uri) {
  44. final GetObjectRequest getObjectRequest =
  45. GetObjectRequest.builder()
  46. .bucket(s3Uri.getHost())
  47. .key(s3Uri.getPath().substring(1))
  48. .overrideConfiguration(builder -> builder.signer(awsSigner))
  49. .build();
  50. final Region bucketRegion = bucketRegionMap.computeIfAbsent(s3Uri.getHost(),
  51. bucketName -> {
  52. final GetBucketLocationRequest getBucketLocationRequest = GetBucketLocationRequest.builder()
  53. .bucket(bucketName)
  54. .build();
  55. return Region.of(s3Client.getBucketLocation(getBucketLocationRequest).locationConstraint().toString());
  56. });
  57. final GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
  58. .signatureDuration(Duration.ofSeconds(0)) // required, but ignored
  59. .getObjectRequest(getObjectRequest)
  60. .build();
  61. return presignerMap.computeIfAbsent(bucketRegion, this::buildPresignerForRegion).presignGetObject(getObjectPresignRequest).url().toURI();
  62. }

对于上面注入的CustomAwsSigner。关键的区别是我抛出了一个不支持的操作异常。

  1. import org.jetbrains.annotations.TestOnly;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.stereotype.Component;
  4. import software.amazon.awssdk.auth.signer.AwsS3V4Signer;
  5. import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
  6. import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams;
  7. import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
  8. import software.amazon.awssdk.core.signer.Presigner;
  9. import software.amazon.awssdk.core.signer.Signer;
  10. import software.amazon.awssdk.http.SdkHttpFullRequest;
  11. import java.time.Clock;
  12. import java.time.Instant;
  13. import java.time.ZoneId;
  14. import java.time.ZonedDateTime;
  15. import java.time.temporal.ChronoField;
  16. import java.time.temporal.ChronoUnit;
  17. /**
  18. * This is a custom signer where the expiration is preset to a 5 minute block within an hour.
  19. * This must only be used for presigning.
  20. */
  21. @Component
  22. public class CustomAwsSigner implements Signer, Presigner {
  23. private final AwsS3V4Signer theSigner = AwsS3V4Signer.create();
  24. /**
  25. * This is the clip time for the expiration. This should be divisible into 60.
  26. */
  27. @Value("${aws.s3.clipTimeInMinutes:5}")
  28. private long clipTimeInMinutes;
  29. @Value("${aws.s3.expirationInSeconds:3600}")
  30. private long expirationInSeconds;
  31. /**
  32. * Computes the base time as the processing time to the floor of nearest clip block.
  33. *
  34. * @param processingDateTime processing date time
  35. * @return base time
  36. */
  37. @TestOnly
  38. public ZonedDateTime computeBaseTime(final ZonedDateTime processingDateTime) {
  39. return processingDateTime
  40. .truncatedTo(ChronoUnit.MINUTES)
  41. .with(temporal -> temporal.with(ChronoField.MINUTE_OF_HOUR, temporal.get(ChronoField.MINUTE_OF_HOUR) / clipTimeInMinutes * clipTimeInMinutes));
  42. }
  43. @Override
  44. public SdkHttpFullRequest presign(final SdkHttpFullRequest request, final ExecutionAttributes executionAttributes) {
  45. final Instant baselineInstant = computeBaseTime(ZonedDateTime.now()).toInstant();
  46. final Aws4PresignerParams signingParams = Aws4PresignerParams.builder()
  47. .awsCredentials(executionAttributes.getAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS))
  48. .signingName(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME))
  49. .signingRegion(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
  50. .signingClockOverride(Clock.fixed(baselineInstant, ZoneId.of("UTC")))
  51. .expirationTime(baselineInstant.plus(expirationInSeconds, ChronoUnit.SECONDS))
  52. .build();
  53. return theSigner.presign(request, signingParams);
  54. }
  55. @Override
  56. public SdkHttpFullRequest sign(final SdkHttpFullRequest request, final ExecutionAttributes executionAttributes) {
  57. throw new UnsupportedOperationException("this class is only used for presigning");
  58. }
  59. }
展开查看全部
bvjveswy

bvjveswy5#

如果有人在使用golang预签名具有缓存可能性的URL时遇到困难,您可以创建一个自定义的签名处理程序,并将命名的处理程序与您自己的交换,以更改签名时间并使时间桶的URL相同:

  1. import (
  2. "time"
  3. "github.com/aws/aws-sdk-go/aws/request"
  4. v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
  5. )
  6. // Will create same url if in the same 15 minutes time bucket
  7. const presignPeriod = 15 * time.Minute
  8. // TimeInterface implements an interface that
  9. // has the a time variable (Now) and a function
  10. // to retrieve the time variable
  11. type TimeInterface struct {
  12. Now time.Time
  13. }
  14. func (t *TimeInterface) NowFunc() time.Time {
  15. return t.Now
  16. }
  17. // getSignTime function returns the signing time
  18. // (initial time) for the time bucket
  19. func getSignTime() time.Time {
  20. now := time.Now().UTC()
  21. signTime := now.Round(presignPeriod)
  22. if signTime.After(now) {
  23. signTime.Add(-presignPeriod)
  24. }
  25. return signTime
  26. }
  27. // CustomSignSDKRequest Implements a custom aws signing
  28. // handler that sets signing time on buckets of
  29. // <presignPeriod> minutes.
  30. // It is used so browsers can cache the result of the
  31. // url for get requests, instead of downloading the resource everytime.
  32. func CustomSignSDKRequest(req *request.Request) {
  33. t := TimeInterface{
  34. Now: getSignTime(),
  35. }
  36. v4.SignSDKRequestWithCurrentTime(req, t.NowFunc)
  37. }
展开查看全部
nnsrf1az

nnsrf1az6#

将解决方案从弗朗西斯科的解决方案扩展到golang v2,沿着这条线的东西可能会起作用:

  1. import (
  2. "context"
  3. "log"
  4. "net/http"
  5. "time"
  6. "github.com/aws/aws-sdk-go-v2/aws"
  7. v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
  8. "github.com/aws/aws-sdk-go-v2/service/s3"
  9. )
  10. // Lifted from https://stackoverflow.com/a/70249671
  11. // Will create same url if in the same 15 minutes time bucket
  12. const presignPeriod = 15 * time.Minute
  13. // getSignTime function returns the signing time
  14. // (initial time) for the time bucket
  15. func getSignTime() time.Time {
  16. now := time.Now().UTC()
  17. signTime := now.Round(presignPeriod)
  18. if signTime.After(now) {
  19. signTime = signTime.Add(-presignPeriod)
  20. }
  21. return signTime
  22. }
  23. // HTTPPresignerV4 is a wrapper around the v4.Signer that implements the
  24. // PresignHTTP method.
  25. type HTTPPresignerV4 struct {
  26. signer v4.Signer
  27. }
  28. func (client *HTTPPresignerV4) PresignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request,
  29. payloadHash string, service string, region string, signingTime time.Time,
  30. optFns ...func(*v4.SignerOptions),
  31. ) (url string, signedHeader http.Header, err error) {
  32. // Get the signing time
  33. signTime := getSignTime()
  34. // Sign the request
  35. signedUrl, signedHeaders, err := client.signer.PresignHTTP(context.Background(), credentials, r, payloadHash, service, region, signTime)
  36. if err != nil {
  37. log.Printf("Failed to sign URL: %v", err)
  38. return "", nil, err
  39. }
  40. // Return the URL
  41. return signedUrl, signedHeaders, nil
  42. }
  43. type S3PresignClient struct {
  44. PresignClient *s3.PresignClient
  45. Bucket string
  46. }
  47. func (client *S3PresignClient) GetPresignedUrl(key string) (string, error) {
  48. // Create the request
  49. signedRequest, err := client.PresignClient.PresignGetObject(context.TODO(), &s3.GetObjectInput{
  50. Bucket: aws.String(client.Bucket),
  51. Key: aws.String(key),
  52. })
  53. if err != nil {
  54. log.Printf("Failed to create presigned URL request: %v", err)
  55. return "", err
  56. }
  57. if err != nil {
  58. log.Printf("Failed to sign URL: %v", err)
  59. return "", err
  60. }
  61. // Return the URL
  62. return signedRequest.URL, nil
  63. }
  64. // Create a new PresignClient
  65. func NewPresignClient(config aws.Config, bucket string) *S3PresignClient {
  66. // Here we create a presigning client that has the signingTime set to the
  67. // beginning of the time bucket. This is so that we can reuse the same
  68. // presigned URL for the same time bucket. This is useful to being able to
  69. // generate the same url for the same time bucket and allow some caching of
  70. // the presigned URL.
  71. s3Client := s3.NewFromConfig(config)
  72. presignClient := s3.NewPresignClient(s3Client, func(o *s3.PresignOptions) {
  73. o.Presigner = &HTTPPresignerV4{
  74. signer: *v4.NewSigner(),
  75. }
  76. })
  77. return &S3PresignClient{
  78. PresignClient: presignClient,
  79. Bucket: bucket,
  80. }
  81. }
展开查看全部

相关问题