设计与实践指南)
AWS SDK for Java v2 请求预签名Request Presigner设计与实践指南【免费下载链接】aws-sdk-java-v2The official AWS SDK for Java - Version 2项目地址: https://gitcode.com/GitHub_Trending/aw/aws-sdk-java-v2本篇技术指南基于 aws-sdk-java-v2 仓库中的设计文档 docs/design/core/presigners/README.md 展开系统讲解 2.x SDK 如何通过 SigV4 查询参数签名query parameter signing生成可交付第三方在指定时间窗口内执行的预签名请求Presigned Request与预签名 URLPresigned URL。你将掌握S3Presigner的完整 API 设计、PresignedRequest的能力模型、浏览器兼容性判定方法、Java 代码中的两种执行方式以及该功能从手写实现到全量代码生成的整体演进路线并看到每项设计在仓库源码中的真实落地形态。什么是请求预签名与预签名 URL请求预签名Request Presigning允许签名创建者signature creator使用自己的秘密签名凭证signing credentials生成一个 AWS 请求。这个预签名请求可以被另一个独立的签名使用者signature user在固定时间窗口内直接执行无需任何额外认证。典型的应用场景后端服务的支持工程师需要临时把服务日志分享给客户——(1) 将日志上传到 S3(2) 对 S3 GetObject 请求做预签名(3) 将预签名请求发送给客户。客户即可凭此请求下载日志且请求在支持工程师签名时指定的“过期时间”到达之前一直有效。设计文档对两个概念做了严格区分预签名请求Presigned Request任何以查询参数签名方式签署、目的是让他方在未来某个时刻执行的请求。预签名 URLPresigned URL满足以下三个条件的预签名请求——(1) 不包含 payload(2) 不包含 content-type 或x-amz-*头(3) 使用 GET HTTP 方法。这一区分之所以重要是因为满足条件的预签名 URL 可以被浏览器“零配置”直接打开执行。S3 GetObject 的浏览器兼容边界按上述定义一个预签名的 S3GetObjectRequest只有在其不包含下列任一字段时才称得上预签名 URL字段对应请求头sseCustomerAlgorithmx-amz-server-side-encryption-customer-algorithmsseCustomerKeyx-amz-server-side-encryption-customer-keysseCustomerKeyMD5x-amz-server-side-encryption-customer-key-MD5requesterPaysx-amz-request-payer原因在于这些字段对应的请求头在签名时被纳入了签名内容signed headers但浏览器发起 GET 时并不会携带这些头服务端校验签名时就会报 signature mismatch 错误。在最终落地实现中PresignedRequest#isBrowserExecutable() 被设计为“当 HTTP 方法是 GET 且签名所包含的所有数据都能被标准浏览器发出时为 true”正是对文档中该判定的直接实现。整体 API 设计SDK 2.x 同时支持预签名请求与预签名 URLAPI 设计让二者易于区分并保证生成的预签名请求能够被任何实现了 SDK HTTP Client 阻塞/非阻塞 SPI 的 HTTP 客户端执行。为快速回应社区对 S3 预签名 URL 的强烈诉求第一版迭代只支持生成 S3 GetObject 的预签名请求后续里程碑再扩展其他操作、服务与代码生成型预签名器。这一设计最终落地为一组真实类services/s3/src/main/java/software/amazon/awssdk/services/s3/presignerS3Presigner预签名入口接口model/下 9 对输入/输出类型GetObjectPresignRequest/PresignedGetObjectRequest、PutObjectPresignRequest/PresignedPutObjectRequest、HeadObjectPresignRequest/PresignedHeadObjectRequest、HeadBucketPresignRequest/PresignedHeadBucketRequest、DeleteObjectPresignRequest/PresignedDeleteObjectRequest、CreateMultipartUploadPresignRequest/PresignedCreateMultipartUploadRequest、UploadPartPresignRequest/PresignedUploadPartRequest、CompleteMultipartUploadPresignRequest/PresignedCompleteMultipartUploadRequest、AbortMultipartUploadPresignRequest/PresignedAbortMultipartUploadRequest。使用示例示例 1生成预签名请求S3Presigner s3Presigner S3Presigner.create(); PresignedGetObjectRequest presignedRequest s3Presigner.presignGetObject(r - r.getObject(get - get.bucket(bucket).key(key)) .signatureDuration(Duration.ofMinutes(15))); URL url presignedRequest.url();示例 2判断预签名请求能否在浏览器中打开S3Presigner s3Presigner S3Presigner.create(); PresignedGetObjectRequest presignedRequest s3Presigner.presignGetObject(...); Validate.isTrue(presignedRequest.isBrowserCompatible()); System.out.println(Click the following link to download the object: presignedRequest.url());注意设计文档中的方法名isBrowserCompatible()在正式发布版中已更名为 PresignedRequest#isBrowserExecutable()文档中关于方法名的 TODO 最终得到了落实。示例 3用 URL Connection HTTP 客户端执行预签名请求S3Presigner s3Presigner S3Presigner.create(); PresignedGetObjectRequest presignedRequest s3Presigner.presignGetObject(...); try (SdkHttpClient httpClient UrlConnectionHttpClient.create()) { ContentStreamProvider payload presignedRequest.payload() .map(SdkBytes::asInputStream) .map(is - () - is) .orElse(null); HttpExecuteRequest executeRequest HttpExecuteRequest.builder() .request(presignedRequest.httpRequest()) .contentStreamProvider(payload) .build(); HttpExecuteResponse response httpClient.prepareRequest(executeRequest).call(); Validate.isTrue(response.httpResponse().isSuccessful()); }{Service}Presigner每个服务都会生成一个{Service}Presigner类如S3Presigner沿用现有{Service}Client与{Service}Utilities的命名策略。接口设计如下摘自原文档/** * Allows generating presigned URLs for supported S3 operations. */ public interface S3Presigner { static S3Presigner create(); static S3Presigner.Builder builder(); /** * Presign a GetObjectRequest so that it can be invoked directly via an HTTP client. */ PresignedGetObjectRequest presignGetObject(GetObjectPresignRequest request); PresignedGetObjectRequest presignGetObject(ConsumerGetObjectPresignRequest.Builder request); interface Builder { Builder region(Region region); Builder credentialsProvider(AwsCredentialsProvider credentials); Builder endpointOverride(URL endpointOverride); // ... S3Presigner build(); } }该接口在仓库中的真实实现 S3Presigner 继承了核心层的基础接口 SdkPresigner含close()与通用Builder并最终由DefaultS3Presignerservices/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java实现。实例化方式Create 方法使用默认 region / 凭证链与S3Client.create()一致。源码中S3Presigner.create()直接委托给builder().build()region 通过DefaultAwsRegionProviderChain加载、凭证通过DefaultCredentialsProvider加载。S3Presigner s3Presigner S3Presigner.create();Builder可配置 region / 凭证与S3Client.builder().build()一致。S3Presigner s3Presigner S3Presigner.builder().region(Region.US_WEST_2).build();从已有 S3Client 获取复用已有S3Client实例的 region / 凭证类似s3.utilities()文档中标注实现日期为 TBD。S3Client s3 S3Client.create(); S3Presigner s3Presigner s3.presigner();从 S3 gateway 类获取实现日期 TBDcreate()与builder()的可发现别名。S3Presigner s3Presigner S3.presigner(); S3Presigner s3Presigner S3.presignerBuilder().build();方法每个支持预签名的操作会生成一对方法Presigned{Operation}Request presign{Operation}({Operation}PresignRequest)及其ConsumerBuilder便捷重载例如PresignedGetObjectRequest presignGetObject(GetObjectPresignRequest)。在真实源码中S3Presigner 为 9 个操作GetObject、HeadObject、HeadBucket、PutObject、DeleteObject、CreateMultipartUpload、UploadPart、CompleteMultipartUpload、AbortMultipartUpload各提供了完整实现其中presignGetObject、presignPutObject等由DefaultS3Presigner覆盖实现其余如presignHeadObject、presignHeadBucket在当前版本中为default方法并默认抛UnsupportedOperationException。内部类{Service}Presigner.Builder预签名器 Builder 至少包含以下配置项原文档设计region(Region)生成预签名 URL 时使用的 region。endpointOverride(URI)替代从 region 推导出的 endpoint。credentialsProvider(AwsCredentialsProvider)签名请求时使用的凭证。在设计文档之后真实实现的 SdkPresigner.Builder 进一步扩展了credentialsProvider(IdentityProvider? extends AwsCredentialsIdentity)两个重载以“后调用的生效”为规则、dualstackEnabled(Boolean)、fipsEnabled(Boolean)并支持从系统属性、环境变量、~/.aws/credentials/config文件、ECS/EC2 元数据服务等渠道自动探测。S3 专属的 S3Presigner.Builder 还增加了serviceConfiguration(S3Configuration)、s3Client(S3Client)与disableS3ExpressSessionAuth(Boolean)。{Operation}PresignRequest每个支持预签名的操作都会生成一个输入类如GetObjectPresignRequest并继承公共基类PresignRequestcore/aws-core/src/main/java/software/amazon/awssdk/awscore/presigner/PresignRequest.java以便复用公共配置逻辑。/** * A request to generate presigned GetObjectRequest, passed to S3Presigner#getObject. */ public interface GetObjectPresignRequest extends PresignRequest { /** * The GetObjectRequest that should be presigned. */ GetObjectRequest getObject(); // Plus builder boilerplate } public interface PresignRequest { /** * The duration for which this presigned request should be valid. After this time has expired, * attempting to use the presigned request will fail. */ Duration signatureDuration(); // Plus builder boilerplate }真实实现 GetObjectPresignRequest 为不可变类构造时通过Validate.notNull(builder.getObjectRequest, getObjectRequest)强制校验操作请求非空并通过signatureDuration控制有效期。从 S3Presigner 的类级文档可知预签名请求有效期最长 7 天超期在生成时即会失败对 S3Express 请求因会话凭证仅存活 5 分钟签名时长上限为5 分钟。Presigned{Operation}Request每个支持预签名的操作生成一个输出类如PresignedGetObjectRequest继承公共基类PresignedRequestcore/aws-core/src/main/java/software/amazon/awssdk/awscore/presigner/PresignedRequest.java。原文档定义的公共能力如下/** * A presigned GetObjectRequest, returned by S3Presigner#getObject. */ public interface PresignedGetObjectRequest extends PresignedRequest { // Builder boilerplate } /** * A generic presigned request. The isBrowserCompatible method can be used to determine whether this request * can be executed by a web browser. */ public interface PresignedRequest { URL url(); // 预签名请求将要执行的目标 URL Instant expiration(); // 请求过期的精确 SERVICE 时间可能与本地时钟存在偏差 boolean isBrowserCompatible(); // HTTP 方法为 GET 且无 signed headers / signed payload 时为 true boolean hasSignedHeaders(); // 是否存在已签名头必须随请求发送否则 signature mismatch MapString, ListString signedHeaders(); // 已签名头子集必须原样携带 boolean hasSignedPayload(); // 是否存在已签名 payload OptionalSdkBytes signedPayload(); // 被签名的 payloadhasSignedPayload 为 false 时为空 SdkHttpRequest httpRequest(); // 完整的 SigV4 查询参数签名请求不含 payload // 可被任何实现 SDK HTTP client SPI 的客户端原样发送 // Plus builder boilerplate }落地后的 PresignedRequest 以url由httpRequest.getUri()转换而来、expiration、isBrowserExecutable、signedHeaders、signedPayload、httpRequest六个字段保存结果构造时对expiration、isBrowserExecutable、signedHeaders、httpRequest做非空校验并实现equals/hashCode。SdkBytes类型的signedPayload可借助asContentStreamProvider()/asInputStream()直接作为 HTTP 请求体发送。里程碑Milestones设计文档按以下 7 个里程碑规划实现路线当前仓库中 M1、M2 及 S3 全套手写预签名器已落地M1手写 S3 GetObject 预签名器。完成条件客户可使用 SDK 提供的 S3 预签名器生成 S3 GetObject 预签名请求。任务包括手写文档所述接口与类定义、暂不创建S3Client#presigner方法、以最小化核心类重构实现presignGetObject。M2手写 S3 PutObject 预签名器。完成条件可生成 S3 PutObject 预签名请求。M3手写 Polly SynthesizeSpeech 预签名器。完成条件可生成 Polly SynthesizeSpeech 预签名请求。需要手写一个生成浏览器兼容 HTTP 请求的SynthesizeSpeechmarshaller并考虑是否将其设为默认。M4生成式预签名器Generated Presigners。完成条件现有预签名器改由代码生成客户无需任何代码改动。任务包括重构核心类消除presignGetObject/presignPutObject/presignSynthesizeSpeech之间的重复、支持浏览器兼容 marshaller 的自定义开关、更新代码生成以产出presign*输入/输出与{Service}Presigner类。M5生成 1.11.x 已有预签名器。完成条件1.11.x 中存在的操作预签名器在 2.x 中可用。任务包括生成 EC2 预签名器、生成 RDS 预签名器。M6生成全部预签名器。完成条件所有可支持预签名的操作均支持预签名。任务包括对代表性操作样本做生成预签名器测试。M7实例化与可发现性简化。完成条件所有客户端包含{Service}Client#presigner()方法。任务包括确定需从服务客户端继承的配置及其继承方式如执行拦截器如何工作、确定异步客户端是否需要独立的预签名器接口为未来阻塞式 region/凭证 provider 留出前向兼容、更新生成的客户端以合理方式继承配置、为所有支持的服务生成该方法。FAQ设计决策详解为哪些服务生成 URL 预签名器只要某个服务存在需要预签名支持的操作就为其生成{Service}Presigner类。为哪些操作生成 URL 预签名器支持的操作集合随实现里程碑变化见上文。文档假设的支持集合是除带签名、流式 payload 的操作外的全部操作——带签名的流式 payload 需要额外的建模如分块编码 payload 或事件流若有足够客户需求可在后续补充设计后实现。为什么每个操作使用不同的输入形状输入形状必须感知操作输入可选方案为(1) 每个操作一个独立的生成输入形状GetObjectPresignRequest或 (2) 一个公共“核心”输入形状并用操作输入参数化PresignRequestGetObject。文档以两段代码对比两种方案方案 1presignGetObject(GetObjectPresignRequest)与presignGetObject(ConsumerGetObjectPresignRequest.Builder)s3.presignGetObject(GetObjectPresignRequest.builder() .getObject(GetObjectRequest.builder().bucket(bucket).key(key).build()) .signatureDuration(Duration.ofMinutes(15)) .build());s3.presignGetObject(r - r.signatureDuration(Duration.ofMinutes(15)) .getObject(go - go.bucket(bucket).key(key)));方案 2presignGetObject(PresignRequestGetObject)与presignGetObject(GetObject, ConsumerPresignRequest.BuilderGetObject)s3.presignGetObject(PresignRequest.builder(GetObjectRequest.builder().bucket(bucket).key(key).build()) .signatureDuration(Duration.ofMinutes(15)) .build());s3.presignGetObject(GetObjectRequest.builder().bucket(bucket).key(key).build(), r - r.signatureDuration(Duration.ofMinutes(15)));方案 1 优点对入门级开发者更易读更贴近S3Client签名构造更简单无需先构造操作输入再实例化 builderConsumerBuilder方法变体简单得多。方案 2 优点jar 体积更小。最终采用方案 1这一决策在真实仓库中得到验证——S3Presigner为每个操作都定义了专属的{Operation}PresignRequest输入类。为什么每个操作使用不同的输出形状输出形状技术上无需感知输入形状可选方案为(1) 每个操作独立的生成输出形状或 (2) 直接返回PresignedRequest。方案 1PresignedGetObjectRequest presignGetObject(GetObjectPresignRequest)PresignedGetObjectRequest presignedRequest s3.presignGetObject(...); URL presignedUrl presignedRequest.getUrl();方案 2PresignedRequest presignGetObject(GetObjectPresignRequest)PresignedRequest presignedRequest s3.presignGetObject(...); URL presignedUrl presignedRequest.getUrl();方案 1 优点可为预签名请求的类型安全执行提供可能最接近S3Client方法签名。方案 2 优点jar 体积更小。决策采用方案 1。理由是空接口的成本极低且它为未来支持预签名请求的类型安全执行奠定基础。仓库中Presigned{Operation}Request如 PresignedGetObjectRequest正是这样一套按操作生成、继承公共基类的类型。预签名请求的执行Execution上述设计已使签名请求能够被任何实现 AWS SDK HTTP client SPI 的客户端执行。未来若允许签名使用者借助完整 SDK 栈而非仅 HTTP client执行预签名 URL将带来三方面收益网络或服务故障时的自动重试automatic retries建模响应的响应形状反序列化response shape unmarshallingSDK 指标集成SDK metric integration一旦实现。文档给出一个示意非设计提案若DynamoDbClient支持执行预签名 URL应确保请求对应正确的操作以便重试与响应处理匹配服务/操作DynamoDbClient dynamo DynamoDbClient.create(); PresignedPutItemRequest presignedRequest dynamo.presigner().presignPutItem(...); PutItemResponse response dynamo.putItem(presignedRequest);非阻塞Non-Blocking请求预签名上述提案不区分阻塞与非阻塞预签名因为当前 SDK 仅在 HTTP 客户端实现层面区分阻塞与非阻塞。生成的预签名请求既可以由阻塞 HTTP 客户端执行也可以由非阻塞 HTTP 客户端执行。未来若 SDK 实现非阻塞 region provider 与非阻塞凭证 provider届时才需要区分阻塞/非阻塞 URL 预签名器。因此需要决策获取预配置 URL 预签名器的presigner()方法是仅放在阻塞型{Service}Client上为{Service}AsyncClient提供独立的非阻塞{Service}Presigner异步类还是让同步与异步客户端共用同一个{Service}Presigner。从设计到实现的演进要点对比设计文档与仓库现状可看到以下关键落地差异与增强供读者在实际编码时参考方法命名isBrowserCompatible()最终定名为isBrowserExecutable()语义从“判断 GET 无签名头/payload”细化到“签名所包含的所有数据都能被标准浏览器发出”。有效期约束文档只提“固定时间窗口”实现在 S3Presigner 中明确了普通请求最长 7 天、S3Express 请求最长 5 分钟的硬限制。浏览器兼容性受配置影响启用校验和验证checksum validation会使 URL 失去浏览器兼容性会新增必须随请求携带的签名头。预签名器默认关闭该校验但当使用自定义 S3Configuration如启用 path style access 或 accelerate 模式时必须显式关闭S3Presigner presigner S3Presigner.builder() .serviceConfiguration(S3Configuration.builder() .checksumValidationEnabled(false) .build()) .build();S3Express 支持当请求指向 S3Express 桶时默认使用常规 IAM/SigV4 凭证寿命更长签名若希望使用短命的 S3 会话凭证签名需通过s3Client(S3Client)提供可额外调用 S3 获取会话凭证的客户端或用disableS3ExpressSessionAuth(true)显式禁用会话认证详见 S3Presigner 的类级文档与 Builder 方法注释。生命周期管理SdkPresigner继承SdkAutoCloseable官方建议在应用启动时创建一次预签名器创建成本较高使用完毕后调用close()释放凭证 provider 等底层资源例如配置为 assume STS role 的 profile。从 Java 代码执行预签名请求除浏览器外所有预签名请求均可从 Java 代码执行S3Presigner 类级文档给出两种方式方式一JDKURLConnection// 使用 S3Presigner 的某个 presign 方法生成预签名请求 PresignedRequest presignedRequest ...; // 创建 JDK HttpURLConnection 与 S3 通信 HttpURLConnection connection (HttpURLConnection) presignedRequest.url().openConnection(); // 携带服务所需请求头isBrowserExecutable 为 true 时无需 presignedRequest.httpRequest().headers().forEach((header, values) - { values.forEach(value - connection.addRequestProperty(header, value)); }); // 发送服务所需 payloadisBrowserExecutable 为 true 时无需 if (presignedRequest.signedPayload().isPresent()) { connection.setDoOutput(true); try (InputStream signedPayload presignedRequest.signedPayload().get().asInputStream(); OutputStream httpOutputStream connection.getOutputStream()) { IoUtils.copy(signedPayload, httpOutputStream); } } // 读取执行结果 try (InputStream content connection.getInputStream()) { System.out.println(Service returned response: ); IoUtils.copy(content, System.out); }方式二SDK 同步SdkHttpClient// 使用 S3Presigner 的某个 presign 方法生成预签名请求 PresignedRequest presignedRequest ...; // 使用 SDK 提供的任一实现创建 SdkHttpClient SdkHttpClient httpClient ApacheHttpClient.builder().build(); // 或 UrlConnectionHttpClient.create() // 构造请求 payloadisBrowserExecutable 为 true 时无需 ContentStreamProvider requestPayload presignedRequest.signedPayload() .map(SdkBytes::asContentStreamProvider) .orElse(null); // 组装并发送请求 HttpExecuteRequest request HttpExecuteRequest.builder() .request(presignedRequest.httpRequest()) .contentStreamProvider(requestPayload) .build(); HttpExecuteResponse response httpClient.prepareRequest(request).call(); // 读取响应体 if (response.responseBody().isPresent()) { try (InputStream responseStream response.responseBody().get()) { System.out.println(Service returned response: ); IoUtils.copy(responseStream, System.out); } }两种方式均充分利用了PresignedRequest#httpRequest()与signedPayload()前者给出可直接透传的完整签名请求含签名与未签名头后者给出必须随请求发送的已签名 payload从而保证服务端签名校验通过。延伸阅读功能设计文档docs/design/core/presigners/README.md公共 API 实现S3Presigner、SdkPresigner、PresignedRequest、PresignRequest内部实现DefaultS3Presigner、DefaultSdkPresigner预签名模型类services/s3/src/main/java/software/amazon/awssdk/services/s3/presigner/model服务配置S3Configuration【免费下载链接】aws-sdk-java-v2The official AWS SDK for Java - Version 2项目地址: https://gitcode.com/GitHub_Trending/aw/aws-sdk-java-v2创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考