检查对象完整性 - Amazon Simple Storage Service

检查对象完整性

Amazon S3 使用校验和值验证您上传到 Amazon S3 或从 Amazon S3 下载的数据的完整性。此外,您可以请求为存储在 Amazon S3 中的任何对象计算另一个校验和值。您可以从几种校验和算法中进行选择,以便在上传或复制数据时使用。Amazon S3 使用此算法计算额外的校验和值并将其存储为对象元数据的一部分。要详细了解如何使用其他校验和来验证数据完整性,请参阅教程:使用其他校验和检查 Amazon S3 中数据的完整性

在上传对象时,您可以选择将预先计算的校验和作为请求的一部分包含在内。Amazon S3 将提供的校验和与它使用指定算法计算出的校验和进行比较。如果这两个值不匹配,Amazon S3 会报告错误。

使用支持的校验和算法

Amazon S3 为您提供了选择校验和算法的选项,该算法用于在上传或下载期间验证数据。您可以选择以下安全哈希算法 (SHA) 或循环冗余校验 (CRC) 数据完整性检查算法之一:

  • CRC32

  • CRC32C

  • SHA-1

  • SHA-256

在上传对象时,可指定要使用的算法:

  • 使用 AWS Management Console 时,选择要使用的校验和算法。当您这样做时,可以选择指定对象的校验和值。当 Amazon S3 收到对象时,它使用您指定的算法计算校验和。如果这两个校验和值不匹配,Amazon S3 会生成错误。

  • 在使用 SDK 时,您可以将 x-amz-sdk-checksum-algorithm 参数的值设置为您希望 Amazon S3 在计算校验和时使用的算法。Amazon S3 会自动计算校验和值。

  • 在使用 REST API 时,不使用 x-amz-sdk-checksum-algorithm 参数,而是使用一个特定于算法的标头(例如,x-amz-checksum-crc32)。

有关上传对象的更多信息,请参阅上传对象

要将这些校验和值中的任何一个应用于已上传到 Amazon S3 的对象,您可以复制该对象。复制对象时,可以指定是使用现有校验和算法还是使用新校验和算法。在使用任何受支持的机制复制对象(包括 S3 批量操作)时,您可以指定校验和算法。有关 S3 批量操作的更多信息,请参阅 对 Amazon S3 对象执行大规模批量操作

重要

如果您结合其他校验和使用分段上传,则分段编号必须是连续的编号。使用其他校验和时,如果您尝试使用非连续分段编号完成分段上传请求,Amazon S3 将生成 HTTP 500 Internal Server Error 错误。

上传对象后,您可以获取校验和值,并将其与预先计算的或之前存储的使用相同算法计算出的校验和值进行比较。

要了解有关使用控制台和指定在上传对象时要使用的校验和算法的更多信息,请参阅上传对象教程:使用其他校验和检查 Amazon S3 中数据的完整性

以下示例显示了如何使用 AWS SDK 通过分段上传来上传大文件、下载大文件和验证分段上传文件,所有这些都使用 SHA-256 进行文件验证。

Java
例 示例:使用 SHA-256 上传、下载和验证大文件

有关创建和测试有效示例的说明,请参阅测试 Amazon S3 Java 代码示例

import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.AbortMultipartUploadRequest; import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm; import software.amazon.awssdk.services.s3.model.ChecksumMode; import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest; import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse; import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload; import software.amazon.awssdk.services.s3.model.CompletedPart; import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest; import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse; import software.amazon.awssdk.services.s3.model.GetObjectAttributesRequest; import software.amazon.awssdk.services.s3.model.GetObjectAttributesResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.GetObjectTaggingRequest; import software.amazon.awssdk.services.s3.model.ObjectAttributes; import software.amazon.awssdk.services.s3.model.PutObjectTaggingRequest; import software.amazon.awssdk.services.s3.model.Tag; import software.amazon.awssdk.services.s3.model.Tagging; import software.amazon.awssdk.services.s3.model.UploadPartRequest; import software.amazon.awssdk.services.s3.model.UploadPartResponse; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Base64; import java.util.List; public class LargeObjectValidation { private static String FILE_NAME = "sample.file"; private static String BUCKET = "sample-bucket"; //Optional, if you want a method of storing the full multipart object checksum in S3. private static String CHECKSUM_TAG_KEYNAME = "fullObjectChecksum"; //If you have existing full-object checksums that you need to validate against, you can do the full object validation on a sequential upload. private static String SHA256_FILE_BYTES = "htCM5g7ZNdoSw8bN/mkgiAhXt5MFoVowVg+LE9aIQmI="; //Example Chunk Size - this must be greater than or equal to 5MB. private static int CHUNK_SIZE = 5 * 1024 * 1024; public static void main(String[] args) { S3Client s3Client = S3Client.builder() .region(Region.US_EAST_1) .credentialsProvider(new AwsCredentialsProvider() { @Override public AwsCredentials resolveCredentials() { return new AwsCredentials() { @Override public String accessKeyId() { return Constants.ACCESS_KEY; } @Override public String secretAccessKey() { return Constants.SECRET; } }; } }) .build(); uploadLargeFileBracketedByChecksum(s3Client); downloadLargeFileBracketedByChecksum(s3Client); validateExistingFileAgainstS3Checksum(s3Client); } public static void uploadLargeFileBracketedByChecksum(S3Client s3Client) { System.out.println("Starting uploading file validation"); File file = new File(FILE_NAME); try (InputStream in = new FileInputStream(file)) { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder() .bucket(BUCKET) .key(FILE_NAME) .checksumAlgorithm(ChecksumAlgorithm.SHA256) .build(); CreateMultipartUploadResponse createdUpload = s3Client.createMultipartUpload(createMultipartUploadRequest); List<CompletedPart> completedParts = new ArrayList<CompletedPart>(); int partNumber = 1; byte[] buffer = new byte[CHUNK_SIZE]; int read = in.read(buffer); while (read != -1) { UploadPartRequest uploadPartRequest = UploadPartRequest.builder() .partNumber(partNumber).uploadId(createdUpload.uploadId()).key(FILE_NAME).bucket(BUCKET).checksumAlgorithm(ChecksumAlgorithm.SHA256).build(); UploadPartResponse uploadedPart = s3Client.uploadPart(uploadPartRequest, RequestBody.fromByteBuffer(ByteBuffer.wrap(buffer, 0, read))); CompletedPart part = CompletedPart.builder().partNumber(partNumber).checksumSHA256(uploadedPart.checksumSHA256()).eTag(uploadedPart.eTag()).build(); completedParts.add(part); sha256.update(buffer, 0, read); read = in.read(buffer); partNumber++; } String fullObjectChecksum = Base64.getEncoder().encodeToString(sha256.digest()); if (!fullObjectChecksum.equals(SHA256_FILE_BYTES)) { //Because the SHA256 is uploaded after the part is uploaded; the upload is bracketed and the full object can be fully validated. s3Client.abortMultipartUpload(AbortMultipartUploadRequest.builder().bucket(BUCKET).key(FILE_NAME).uploadId(createdUpload.uploadId()).build()); throw new IOException("Byte mismatch between stored checksum and upload, do not proceed with upload and cleanup"); } CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder().parts(completedParts).build(); CompleteMultipartUploadResponse completedUploadResponse = s3Client.completeMultipartUpload( CompleteMultipartUploadRequest.builder().bucket(BUCKET).key(FILE_NAME).uploadId(createdUpload.uploadId()).multipartUpload(completedMultipartUpload).build()); Tag checksumTag = Tag.builder().key(CHECKSUM_TAG_KEYNAME).value(fullObjectChecksum).build(); //Optionally, if you need the full object checksum stored with the file; you could add it as a tag after completion. s3Client.putObjectTagging(PutObjectTaggingRequest.builder().bucket(BUCKET).key(FILE_NAME).tagging(Tagging.builder().tagSet(checksumTag).build()).build()); } catch (IOException | NoSuchAlgorithmException e) { e.printStackTrace(); } GetObjectAttributesResponse objectAttributes = s3Client.getObjectAttributes(GetObjectAttributesRequest.builder().bucket(BUCKET).key(FILE_NAME) .objectAttributes(ObjectAttributes.OBJECT_PARTS, ObjectAttributes.CHECKSUM).build()); System.out.println(objectAttributes.objectParts().parts()); System.out.println(objectAttributes.checksum().checksumSHA256()); } public static void downloadLargeFileBracketedByChecksum(S3Client s3Client) { System.out.println("Starting downloading file validation"); File file = new File("DOWNLOADED_" + FILE_NAME); try (OutputStream out = new FileOutputStream(file)) { GetObjectAttributesResponse objectAttributes = s3Client.getObjectAttributes(GetObjectAttributesRequest.builder().bucket(BUCKET).key(FILE_NAME) .objectAttributes(ObjectAttributes.OBJECT_PARTS, ObjectAttributes.CHECKSUM).build()); //Optionally if you need the full object checksum, you can grab a tag you added on the upload List<Tag> objectTags = s3Client.getObjectTagging(GetObjectTaggingRequest.builder().bucket(BUCKET).key(FILE_NAME).build()).tagSet(); String fullObjectChecksum = null; for (Tag objectTag : objectTags) { if (objectTag.key().equals(CHECKSUM_TAG_KEYNAME)) { fullObjectChecksum = objectTag.value(); break; } } MessageDigest sha256FullObject = MessageDigest.getInstance("SHA-256"); MessageDigest sha256ChecksumOfChecksums = MessageDigest.getInstance("SHA-256"); //If you retrieve the object in parts, and set the ChecksumMode to enabled, the SDK will automatically validate the part checksum for (int partNumber = 1; partNumber <= objectAttributes.objectParts().totalPartsCount(); partNumber++) { MessageDigest sha256Part = MessageDigest.getInstance("SHA-256"); ResponseInputStream<GetObjectResponse> response = s3Client.getObject(GetObjectRequest.builder().bucket(BUCKET).key(FILE_NAME).partNumber(partNumber).checksumMode(ChecksumMode.ENABLED).build()); GetObjectResponse getObjectResponse = response.response(); byte[] buffer = new byte[CHUNK_SIZE]; int read = response.read(buffer); while (read != -1) { out.write(buffer, 0, read); sha256FullObject.update(buffer, 0, read); sha256Part.update(buffer, 0, read); read = response.read(buffer); } byte[] sha256PartBytes = sha256Part.digest(); sha256ChecksumOfChecksums.update(sha256PartBytes); //Optionally, you can do an additional manual validation again the part checksum if needed in addition to the SDK check String base64PartChecksum = Base64.getEncoder().encodeToString(sha256PartBytes); String base64PartChecksumFromObjectAttributes = objectAttributes.objectParts().parts().get(partNumber - 1).checksumSHA256(); if (!base64PartChecksum.equals(getObjectResponse.checksumSHA256()) || !base64PartChecksum.equals(base64PartChecksumFromObjectAttributes)) { throw new IOException("Part checksum didn't match for the part"); } System.out.println(partNumber + " " + base64PartChecksum); } //Before finalizing, do the final checksum validation. String base64FullObject = Base64.getEncoder().encodeToString(sha256FullObject.digest()); String base64ChecksumOfChecksums = Base64.getEncoder().encodeToString(sha256ChecksumOfChecksums.digest()); if (fullObjectChecksum != null && !fullObjectChecksum.equals(base64FullObject)) { throw new IOException("Failed checksum validation for full object"); } System.out.println(fullObjectChecksum); String base64ChecksumOfChecksumFromAttributes = objectAttributes.checksum().checksumSHA256(); if (base64ChecksumOfChecksumFromAttributes != null && !base64ChecksumOfChecksums.equals(base64ChecksumOfChecksumFromAttributes)) { throw new IOException("Failed checksum validation for full object checksum of checksums"); } System.out.println(base64ChecksumOfChecksumFromAttributes); out.flush(); } catch (IOException | NoSuchAlgorithmException e) { //Cleanup bad file file.delete(); e.printStackTrace(); } } public static void validateExistingFileAgainstS3Checksum(S3Client s3Client) { System.out.println("Starting existing file validation"); File file = new File("DOWNLOADED_" + FILE_NAME); GetObjectAttributesResponse objectAttributes = s3Client.getObjectAttributes(GetObjectAttributesRequest.builder().bucket(BUCKET).key(FILE_NAME) .objectAttributes(ObjectAttributes.OBJECT_PARTS, ObjectAttributes.CHECKSUM).build()); try (InputStream in = new FileInputStream(file)) { MessageDigest sha256ChecksumOfChecksums = MessageDigest.getInstance("SHA-256"); MessageDigest sha256Part = MessageDigest.getInstance("SHA-256"); byte[] buffer = new byte[CHUNK_SIZE]; int currentPart = 0; int partBreak = objectAttributes.objectParts().parts().get(currentPart).size(); int totalRead = 0; int read = in.read(buffer); while (read != -1) { totalRead += read; if (totalRead >= partBreak) { int difference = totalRead - partBreak; byte[] partChecksum; if (totalRead != partBreak) { sha256Part.update(buffer, 0, read - difference); partChecksum = sha256Part.digest(); sha256ChecksumOfChecksums.update(partChecksum); sha256Part.reset(); sha256Part.update(buffer, read - difference, difference); } else { sha256Part.update(buffer, 0, read); partChecksum = sha256Part.digest(); sha256ChecksumOfChecksums.update(partChecksum); sha256Part.reset(); } String base64PartChecksum = Base64.getEncoder().encodeToString(partChecksum); if (!base64PartChecksum.equals(objectAttributes.objectParts().parts().get(currentPart).checksumSHA256())) { throw new IOException("Part checksum didn't match S3"); } currentPart++; System.out.println(currentPart + " " + base64PartChecksum); if (currentPart < objectAttributes.objectParts().totalPartsCount()) { partBreak += objectAttributes.objectParts().parts().get(currentPart - 1).size(); } } else { sha256Part.update(buffer, 0, read); } read = in.read(buffer); } if (currentPart != objectAttributes.objectParts().totalPartsCount()) { currentPart++; byte[] partChecksum = sha256Part.digest(); sha256ChecksumOfChecksums.update(partChecksum); String base64PartChecksum = Base64.getEncoder().encodeToString(partChecksum); System.out.println(currentPart + " " + base64PartChecksum); } String base64CalculatedChecksumOfChecksums = Base64.getEncoder().encodeToString(sha256ChecksumOfChecksums.digest()); System.out.println(base64CalculatedChecksumOfChecksums); System.out.println(objectAttributes.checksum().checksumSHA256()); if (!base64CalculatedChecksumOfChecksums.equals(objectAttributes.checksum().checksumSHA256())) { throw new IOException("Full object checksum of checksums don't match S3"); } } catch (IOException | NoSuchAlgorithmException e) { e.printStackTrace(); } } }

您可以发送 REST 请求以上传具有校验和值的对象,通过 PutObject 验证数据的完整性。您还可以使用 GetObjectHeadObject 检索对象的校验和值。

您可以发送 PUT 请求,在单个操作中上传最大 5 GB 的对象。有关更多信息,请参阅《AWS CLI 命令参考》中的 PutObject。您还可以使用 get-objecthead-object 检索已上传对象的校验和以验证数据的完整性。

有关信息,请参阅《AWS Command Line Interface 用户指南》中的 Amazon S3 CLI FAQ

在上传对象时使用 Content-M5

上传后验证对象完整性的另一种方法是在上传对象时提供其 MD5 摘要。如果您计算对象的 MD5 摘要,则可以使用 Content-MD5 标头向 PUT 命令提供摘要。

上传对象后,Amazon S3 会计算对象的 MD5 摘要,并将其与您提供的值进行比较。只有在两个摘要匹配的情况下,请求才会成功。

不要求提供 MD5 摘要,但您可以在上传过程中使用它来验证对象的完整性。

使用 Content-MD5 和 ETag 验证上传的对象

对象的实体标签 (ETag) 表示该对象的特定版本。请注意,ETag 仅反映对对象内容的更改,而不反映对对象元数据的更改。如果只有对象的元数据发生变化,eTag 将保持不变。

根据对象的不同,对象的 ETag 可能是对象数据的 MD5 摘要:

  • 如果对象是由 PutObjectPostObjectCopyObject 操作创建或通过 AWS Management Console创建,并且该对象还是纯文本或使用具有 Amazon S3 托管式密钥的服务器端加密(SSE-S3)进行加密的,则该对象的 ETag 将是其对象数据的 MD5 摘要。

  • 如果对象是由 PutObjectPostObjectCopyObject 操作创建或通过 AWS Management Console创建,并且该对象使用客户提供的密钥 (SSE-C) 或 AWS Key Management Service (AWS KMS) 密钥 (SSE-KMS) 通过服务器端加密进行加密,则该对象的 ETag 将不是其对象数据的 MD5 摘要。

  • 如果对象由 Multipart UploadPart Copy 操作创建,无论使用哪种加密方法,对象的 ETag 都不是 MD5 摘要。如果对象大于 16 MB,则 AWS Management Console会作为分段上传来上传或复制该对象,因此 ETag 不是 MD5 摘要。

对于 ETag 是对象的 Content-MD5 摘要的对象,您可以将对象的 ETag 值与计算出的或之前存储的 Content-MD5 摘要进行比较。

使用尾随校验和

将对象上传到 Amazon S3 时,您可以为对象提供预先计算的校验和,也可以使用 AWS SDK 代表您自动创建尾随校验和。如果您决定使用尾随校验和,Amazon S3 会使用您指定的算法自动生成校验和,并在上传期间使用它验证对象的完整性。

要使用 AWS SDK 时创建尾随校验和,请使用您首选的算法填充 ChecksumAlgorithm 参数。SDK 使用该算法计算对象(或对象分段)的校验和,并自动将其附加到上传请求的末尾。这种行为可以节省您的时间,因为 Amazon S3 一次性执行数据的验证和上传。

重要

如果您使用的是 S3 对象 Lambda,则对 S3 对象 Lambda 的所有请求都使用 s3-object-lambda 而不是 s3。此行为会影响尾随校验和值的签名。有关 S3 对象 Lambda 的更多信息,请参阅 使用 S3 对象 Lambda 转换对象

使用分段级别墅校验和进行分段上传

将对象上传到 Amazon S3 时,它们可以作为单个对象上传,也可以通过分段上传过程进行上传。大于 16 MB 且通过控制台上传的对象将使用分段上传自动上传。有关分段上传的更多信息,请参阅 使用分段上传来上传和复制对象

当对象作为分段上传进行上传时,该对象的 ETag 不是整个对象的 MD5 摘要。Amazon S3 会在上传时计算每个分段的 MD5 摘要。MD5 摘要用于确定最终对象的 ETag。Amazon S3 将 MD5 摘要的字节串联在一起,然后计算这些串联值的 MD5 摘要。创建 ETag 的最后一步是 Amazon S3 在末尾添加一个短横线和分段总数。

例如,考虑使用 ETag 为 C9A5A6878D97B48CC965C1E41859F034-14 的分段上传的对象上传。在本例中,C9A5A6878D97B48CC965C1E41859F034 是串联在一起的所有摘要的 MD5 摘要。-14 表示有 14 个分段与此对象的分段上传相关联。

如果您已为多分段对象启用了其他校验和值,Amazon S3 将使用指定的校验和算法计算每个分段的校验和。已完成对象的校验和的计算方法与 Amazon S3 计算分段上传的 MD5 摘要的方法相同。您可以使用此校验和验证对象的完整性。

要检索有关对象的信息,包括组成整个对象的分段数,可以使用 GetObjectAttributes 操作。借助其他校验和,您还可以恢复每个分段的信息,包括每个分段的校验和值。

或者,您可以使用 GetObjectHeadObject 操作并指定与单个分段对齐的分段编号或字节范围,来获取单个分段的校验和。借助此方法,您可以使用该校验和验证单个分段,而无需等待所有分段都上传完毕后再验证数据完整性。使用此方法时,还可以仅请求未通过完整性测试的单个分段。

由于 Amazon S3 计算分段对象的校验和的方式,复制对象时,对象的校验和值可能会发生变化。如果您使用的是SDK 或 REST API,并且调用 CopyObject,Amazon S3 可以复制大小不超过 CopyObject API 操作限制的任何对象。无论对象是在单个请求中上传还是作为分段上传的一部分上传,Amazon S3 都可以作为单个操作执行此复制。使用 copy 命令,对象的校验和是完整对象的直接校验和。如果对象最初是使用分段上传进行上传的,那么即使数据没有变化,校验和值也会发生变化。

注意

大于 CopyObject API 操作的大小限制的对象必须使用分段复制命令。

重要

当您使用 AWS Management Console执行某些操作时,如果对象大于 16 MB,则 Amazon S3 将使用分段上传。在这种情况下,校验和不是完整对象的直接校验和,而是基于每个分段的校验和值的计算值。

例如,考虑一个大小为 100 MB 的对象,您使用 REST API 作为单个分段直接上传该对象。在这种情况下,校验和是整个对象的校验和。如果您稍后使用控制台重命名该对象、复制对象、更改存储类或编辑元数据,Amazon S3 将使用分段上传功能来更新对象。因此,Amazon S3 为对象创建一个新的校验和值,该值是根据各个分段的校验和值计算的。

前面的控制台操作列表并不是您可以在 AWS Management Console 中执行的所有可能操作的完整列表,这些操作会导致 Amazon S3 使用分段上传功能更新对象。请记住,无论何时使用控制台对大小超过 16 MB 的对象执行操作,校验和值都可能不是整个对象的校验和。