我想将用户个人资料图片存储在 S3 存储桶中,但将这些图像保密。为了做到这一点,每当需要图像时,我都会创建一个预签名的 URL。然而,这每次都会创建一个唯一的 url,这意味着浏览器永远不会缓存图像,我最终会在 GET 请求中支付更多费用。
这是我生成 url 的代码示例,我使用的是 Laravel:
$s3 = \Storage::disk('s3');
$client = $s3->getDriver()->getAdapter()->getClient();
$expiry = new \DateTime('2017-07-25');
$command = $client->getCommand('GetObject', [
'Bucket' => \Config::get('filesystems.disks.s3.bucket'),
'Key' => $key
]);
$request = $client->createPresignedRequest($command, $expiry);
return (string) $request->getUri();
我认为通过指定日期时间而不是时间单位,它会创建相同的网址,但它实际上会在网址中添加剩余的秒数,这是一个示例:
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=AXXXXXXXXXX %2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20170720T112123Z&X-Amz-SignedHeaders=主机&X-Amz-Expires=391117&X-Amz-Signature=XXXXXXXXX
是否可以生成可重复的预签名请求 URL,以便用户浏览器可以缓存图像?
这是我在关注这篇文章后想出的一个Python解决方案。 它使用 freezegun 库来操纵时间,使签名在给定时间内相同。
import time
import datetime
import boto3
from freezegun import freezetime
S3_CLIENT = boto3.client("s3")
SEVEN_DAYS_IN_SECONDS = 604800
MAX_EXPIRES_SECONDS = SEVEN_DAYS_IN_SECONDS
def get_presigned_get_url(bucket: str, key: str, expires_in_seconds: int = MAX_EXPIRES_SECONDS) -> str:
current_timestamp = int(time.time())
truncated_timestamp = current_timestamp - (current_timestamp % expires_in_seconds)
with freeze_time(datetime.datetime.fromtimestamp(truncated_timestamp)):
presigned_url = S3_CLIENT.generate_presigned_url(
ClientMethod="get_object",
Params={
"Bucket": bucket,
"Key": key,
"ResponseCacheControl": f"private, max-age={expires_in_seconds}, immutable",
},
ExpiresIn=expires_in_seconds,
HttpMethod="GET",
)
return presigned_url
也许回复晚了,但我会添加我的方法,以便将来阅读本文的人们受益。
为了强制浏览器缓存启动,每次生成相同的精确 URL 非常重要,直到您特别希望浏览器从服务器重新加载内容。 不幸的是,sdk 中提供的预签名者每次都依赖当前时间戳来生成新的 url。
这个例子是用Java编写的,但它可以很容易地扩展到其他语言
GetObjectRequest 构建器(用于创建预签名 URL)允许覆盖配置。我们可以提供自定义签名者来修改其行为
AwsRequestOverrideConfiguration.builder()
.signer(new CustomAwsS3V4Signer())
.credentialsProvider(<You may need to provide a custom credential provider
here>)))
.build())
GetObjectRequest getObjectRequest =
GetObjectRequest.builder()
.bucket(getUserBucket())
.key(key)
.responseCacheControl("max-age="+(TimeUnit.DAYS.toSeconds(7)+ defaultIfNull(version,0L)))
.overrideConfiguration(overrideConfig)
.build();
public class CustomAwsS3V4Signer implements Presigner, Signer
{
private final AwsS3V4Signer awsSigner;
public CustomAwsS3V4Signer()
{
awsSigner = AwsS3V4Signer.create();
}
@Override
public SdkHttpFullRequest presign(SdkHttpFullRequest request, ExecutionAttributes executionAttributes)
{
Instant baselineInstant = Instant.now().truncatedTo(ChronoUnit.DAYS);
executionAttributes.putAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION,
baselineInstant.plus(3, ChronoUnit.DAYS));
这里我们重写签名时钟来模拟固定时间,最终导致 url 中的过期和签名一致,直到将来的某个日期:
Aws4PresignerParams.Builder builder = Aws4PresignerParams.builder()
.signingClockOverride(Clock.fixed(baselineInstant, ZoneId.of("UTC")));
Aws4PresignerParams signingParams =
extractPresignerParams(builder, executionAttributes).build();
return awsSigner.presign(request, signingParams);
}
}
更多详细信息请参见此处:
https://murf.ai/resources/creating-cache-Friendly-presigned-s3-urls-using-v4signer-q1bbqgk
您可以将经过身份验证的端点添加到您的应用程序中,并在该端点内检索图像,而不是使用预签名的 URL 机制?在您的
img
标签等中使用此 URL。该端点可以缓存图像并为浏览器提供适当的响应标头来缓存图像。
类似于@Aragorn 的概念,但这是更完整的代码。但这又是Java。另外,由于我的应用程序是多区域的,我必须输入区域属性。
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.signer.Signer;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import javax.annotation.PostConstruct;
import javax.validation.constraints.NotNull;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Component
@Slf4j
public class S3Operations {
@Autowired
private Signer awsSigner;
private final Map<Region, S3Presigner> presignerMap = new ConcurrentHashMap<>();
private S3Presigner buildPresignerForRegion(
AwsCredentialsProvider credentialsProvider,
Region region) {
return S3Presigner.builder()
.credentialsProvider(credentialsProvider)
.region(region)
.build();
}
/**
* Convert an S3 URI to a normal HTTPS URI that expires.
*
* @param s3Uri S3 URI (e.g. s3://bucketname/ArchieTest/フェニックス.jpg)
* @return https URI
*/
@SneakyThrows
public URI getExpiringUri(final URI s3Uri) {
final GetObjectRequest getObjectRequest =
GetObjectRequest.builder()
.bucket(s3Uri.getHost())
.key(s3Uri.getPath().substring(1))
.overrideConfiguration(builder -> builder.signer(awsSigner))
.build();
final Region bucketRegion = bucketRegionMap.computeIfAbsent(s3Uri.getHost(),
bucketName -> {
final GetBucketLocationRequest getBucketLocationRequest = GetBucketLocationRequest.builder()
.bucket(bucketName)
.build();
return Region.of(s3Client.getBucketLocation(getBucketLocationRequest).locationConstraint().toString());
});
final GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofSeconds(0)) // required, but ignored
.getObjectRequest(getObjectRequest)
.build();
return presignerMap.computeIfAbsent(bucketRegion, this::buildPresignerForRegion).presignGetObject(getObjectPresignRequest).url().toURI();
}
对于上面注入的
CustomAwsSigner
。关键的区别是我抛出了一个不受支持的操作异常。
import org.jetbrains.annotations.TestOnly;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.auth.signer.AwsS3V4Signer;
import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.signer.Presigner;
import software.amazon.awssdk.core.signer.Signer;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
/**
* This is a custom signer where the expiration is preset to a 5 minute block within an hour.
* This must only be used for presigning.
*/
@Component
public class CustomAwsSigner implements Signer, Presigner {
private final AwsS3V4Signer theSigner = AwsS3V4Signer.create();
/**
* This is the clip time for the expiration. This should be divisible into 60.
*/
@Value("${aws.s3.clipTimeInMinutes:5}")
private long clipTimeInMinutes;
@Value("${aws.s3.expirationInSeconds:3600}")
private long expirationInSeconds;
/**
* Computes the base time as the processing time to the floor of nearest clip block.
*
* @param processingDateTime processing date time
* @return base time
*/
@TestOnly
public ZonedDateTime computeBaseTime(final ZonedDateTime processingDateTime) {
return processingDateTime
.truncatedTo(ChronoUnit.MINUTES)
.with(temporal -> temporal.with(ChronoField.MINUTE_OF_HOUR, temporal.get(ChronoField.MINUTE_OF_HOUR) / clipTimeInMinutes * clipTimeInMinutes));
}
@Override
public SdkHttpFullRequest presign(final SdkHttpFullRequest request, final ExecutionAttributes executionAttributes) {
final Instant baselineInstant = computeBaseTime(ZonedDateTime.now()).toInstant();
final Aws4PresignerParams signingParams = Aws4PresignerParams.builder()
.awsCredentials(executionAttributes.getAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS))
.signingName(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME))
.signingRegion(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
.signingClockOverride(Clock.fixed(baselineInstant, ZoneId.of("UTC")))
.expirationTime(baselineInstant.plus(expirationInSeconds, ChronoUnit.SECONDS))
.build();
return theSigner.presign(request, signingParams);
}
@Override
public SdkHttpFullRequest sign(final SdkHttpFullRequest request, final ExecutionAttributes executionAttributes) {
throw new UnsupportedOperationException("this class is only used for presigning");
}
}
如果有人在使用 golang 来预签名具有缓存可能性的 url 时遇到困难,您可以创建一个自定义签名处理程序,并将指定的处理程序与您自己的处理程序交换,以更改签名时间并使 URL 在某个时间段内保持相同:
import (
"time"
"github.com/aws/aws-sdk-go/aws/request"
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
)
// Will create same url if in the same 15 minutes time bucket
const presignPeriod = 15 * time.Minute
// TimeInterface implements an interface that
// has the a time variable (Now) and a function
// to retrieve the time variable
type TimeInterface struct {
Now time.Time
}
func (t *TimeInterface) NowFunc() time.Time {
return t.Now
}
// getSignTime function returns the signing time
// (initial time) for the time bucket
func getSignTime() time.Time {
now := time.Now().UTC()
signTime := now.Round(presignPeriod)
if signTime.After(now) {
signTime.Add(-presignPeriod)
}
return signTime
}
// CustomSignSDKRequest Implements a custom aws signing
// handler that sets signing time on buckets of
// <presignPeriod> minutes.
// It is used so browsers can cache the result of the
// url for get requests, instead of downloading the resource everytime.
func CustomSignSDKRequest(req *request.Request) {
t := TimeInterface{
Now: getSignTime(),
}
v4.SignSDKRequestWithCurrentTime(req, t.NowFunc)
}
将解决方案从 Francisco Escher 扩展到 golang v2,类似的方法可能会起作用:
import (
"context"
"log"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// Lifted from https://stackoverflow.com/a/70249671
// Will create same url if in the same 15 minutes time bucket
const presignPeriod = 15 * time.Minute
// getSignTime function returns the signing time
// (initial time) for the time bucket
func getSignTime() time.Time {
now := time.Now().UTC()
signTime := now.Round(presignPeriod)
if signTime.After(now) {
signTime = signTime.Add(-presignPeriod)
}
return signTime
}
// HTTPPresignerV4 is a wrapper around the v4.Signer that implements the
// PresignHTTP method.
type HTTPPresignerV4 struct {
signer v4.Signer
}
func (client *HTTPPresignerV4) PresignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request,
payloadHash string, service string, region string, signingTime time.Time,
optFns ...func(*v4.SignerOptions),
) (url string, signedHeader http.Header, err error) {
// Get the signing time
signTime := getSignTime()
// Sign the request
signedUrl, signedHeaders, err := client.signer.PresignHTTP(context.Background(), credentials, r, payloadHash, service, region, signTime)
if err != nil {
log.Printf("Failed to sign URL: %v", err)
return "", nil, err
}
// Return the URL
return signedUrl, signedHeaders, nil
}
type S3PresignClient struct {
PresignClient *s3.PresignClient
Bucket string
}
func (client *S3PresignClient) GetPresignedUrl(key string) (string, error) {
// Create the request
signedRequest, err := client.PresignClient.PresignGetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(client.Bucket),
Key: aws.String(key),
})
if err != nil {
log.Printf("Failed to create presigned URL request: %v", err)
return "", err
}
if err != nil {
log.Printf("Failed to sign URL: %v", err)
return "", err
}
// Return the URL
return signedRequest.URL, nil
}
// Create a new PresignClient
func NewPresignClient(config aws.Config, bucket string) *S3PresignClient {
// Here we create a presigning client that has the signingTime set to the
// beginning of the time bucket. This is so that we can reuse the same
// presigned URL for the same time bucket. This is useful to being able to
// generate the same url for the same time bucket and allow some caching of
// the presigned URL.
s3Client := s3.NewFromConfig(config)
presignClient := s3.NewPresignClient(s3Client, func(o *s3.PresignOptions) {
o.Presigner = &HTTPPresignerV4{
signer: *v4.NewSigner(),
}
})
return &S3PresignClient{
PresignClient: presignClient,
Bucket: bucket,
}
}