--- title: "Object Storage 개요" slug: "storage-objectstorage" tags: ["Object Storage"] updated: 2026-10-22T10:47:58Z published: 2026-10-22T10:47:58Z canonical: "api-gov.ncloud-docs.com/storage-objectstorage" --- > ## Documentation Index > Fetch the complete documentation index at: https://api-gov.ncloud-docs.com/llms.txt > Use this file to discover all available pages before exploring further. # Object Storage 개요 네이버 클라우드 플랫폼 Object Storage 는 스토리지 관리와 사용에 필요한 S3 API를 제공합니다. *버전: Amazon S3 v2006-03-01* ### 호출 도메인(Endpoint) HTTP/HTTPS 프로토콜을 모두 지원하지만 데이터 보호를 위해 HTTPS 사용을 권장합니다. 리전별 호출 도메인 | 리전 | 리전 이름 | 호출 도메인 | | --- | --- | --- | | 수도권 | gov-standard | https://kr.object.gov-ncloudstorage.com | | 남부권 | gov2-standard | https://krs.object.gov-ncloudstorage.com | ### 인증키(Credential) 콘솔의 **My Account** > **계정 및 보안 관리** > **보안 관리** > **접근 관리** 메뉴에서 생성한 API 인증키를 사용합니다. 아래는 S3 API를 사용하여 네이버 클라우드 플랫폼 Object Storage에 접근할 때 사용할 수 있는 모든 오퍼레이션을 설명합니다. 예를 포함하여 오퍼레이션 사용법에 대한 추가 정보를 확인하려면 버킷 오퍼레이션, 오브젝트 오퍼레이션 페이지를 참고하세요. ### 계정 관련 오퍼레이션 계정 레벨에서는 해당 계정에 속한 버킷 목록을 조회하는 오퍼레이션만 지원합니다. 계정의 버킷 개수는 1,000 개로 제한됩니다. | 오퍼레이션 | 설명 | | --- | --- | | [GET Account(List Buckets)](/docs/storage-objectstorage-listbuckets) | 계정에 속한 버킷 목록 조회 | ### 버킷 오퍼레이션 버킷을 생성, 삭제, 조회, 제어하는 오퍼레이션을 설명합니다. *버킷 내의 오브젝트 목록 조회 GET Bucket (List Objects) Version 2 는 지원하지 않습니다.* | 오퍼레이션 | 설명 | | --- | --- | | [PUT Bucket](/docs/storage-objectstorage-putbucket) | 버킷 생성 계정의 버킷 개수는 1,000 개로 제한됨 | | [GET Bucket(List Objects)](/docs/storage-objectstorage-listobjects) | 버킷에 포함된 오브젝트 조회 한 번에 최대 1,000 개의 오브젝트 나열 | | [HEAD Bucket](/docs/storage-objectstorage-headbucket) | 버킷 헤더 조회 | | [DELETE Bucket](/docs/storage-objectstorage-deletebucket) | 빈 버킷 삭제 | | [PUT Bucket ACL](/docs/storage-objectstorage-putbucketacl) | 버킷에 적용할 접근 제어 목록(ACL) 생성 | | [GET Bucket ACL](/docs/storage-objectstorage-getbucketacl) | 버킷에 적용된 접근 제어 목록(ACL) 조회 | | [PUT Bucket CORS](/docs/storage-objectstorage-putbucketcors) | 버킷에 적용할 CORS 설정 생성 | | [GET Bucket CORS](/docs/storage-objectstorage-getbucketcors) | 버킷에 적용된 CORS 설정 조회 | | [DELETE Bucket CORS](/docs/storage-objectstorage-deletebucketcors) | 버킷에 적용된 CORS 설정 삭제 | | [List Multipart Uploads](/docs/storage-objectstorage-listmultipartuploads) | 완료되지 않았거나 취소된 멀티파트 업로드 조회 | ### 오브젝트 오퍼레이션 오브젝트를 생성, 삭제, 조회, 제어하는 오퍼레이션을 설명합니다. | 오퍼레이션 | 설명 | | --- | --- | | [PUT Object](/docs/storage-objectstorage-putobject) | 버킷에 오브젝트 추가(업로드) | | [PUT Object (Copy)](/docs/storage-objectstorage-copyobject) | 오브젝트 복사본 생성 | | [GET Object](/docs/storage-objectstorage-getobject) | 오브젝트 조회(다운로드) | | [HEAD Object](/docs/storage-objectstorage-headobject) | 오브젝트 헤더 조회 | | [DELETE Object](/docs/storage-objectstorage-deleteobject) | 버킷에서 오브젝트 삭제 | | [DELETE Multiple Objects](/docs/storage-objectstorage-deletemultipleobjects) | 버킷에서 멀티파트 오브젝트 삭제 | | [PUT Object ACL](/docs/storage-objectstorage-putobjectacl) | 오브젝트에 적용할 접근 제어 목록(ACL) 생성 | | [GET Object ACL](/docs/storage-objectstorage-getobjectacl) | 오브젝트에 적용된 접근 제어 목록(ACL) 조회 | | [OPTIONS Object](/docs/storage-objectstorage-optionsobjectcors) | CORS 설정을 확인하여 특정 요청을 전송할 수 있는지 조회 | | [Initiate Multipart Upload](/docs/storage-objectstorage-initiatemultipartupload) | 업로드할 파트 집합에 지정할 업로드 ID 생성 | | [Upload Part](/docs/storage-objectstorage-uploadpart) | 업로드 ID와 연관된 오브젝트의 파트 업로드 | | [Complete Multipart Upload](/docs/storage-objectstorage-completemultipartupload) | 업로드 ID와 연관된 파트로 분리된 오브젝트 결합 | | [Abort Multipart Upload](/docs/storage-objectstorage-abortmultipartupload) | 업로드를 중단하고 업로드 ID와 연관된 파트 삭제 | ## 공통 헤더 ### 공통 요청 헤더 다음 표는 네이버 클라우드 플랫폼 Object Storage에서 지원하는 공통 요청 헤더를 설명합니다. 네이버 클라우드 플랫폼 Object Storage 는 다음 표에 나열되지 않은 공통 헤더는 요청이 전송됐더라도 무시합니다. 단, 요청 중 일부는 이 문서에 정의된 바와 같이 다른 헤더를 지원할 수 있습니다. 인증 헤더 생성에 대한 자세한 내용은 "인증 관리" 페이지에서 확인할 수 있습니다. | 헤더 | 설명 | | --- | --- | | Authorization | 필수(AWS Signature Version 4) | | Host | 필수 | | x-amz-date | 필수, Date로 지정될 수 있음 | | x-amz-content-sha256 | 필수 단, 오브젝트 업로드 시 또는 본문에 요청 정보가 포함된 경우에 해당 | | Content-Length | 필수 단, 오브젝트 업로드 시에 해당 청크 인코딩(chunked encoding)이 지원됨 | | Content-MD5 | 전송 중인 요청 본문의 128 비트 MD5 해시값 | | Expect | 100-continue인 경우 요청 본문 전송 전에 헤더가 승인될 때까지 대기 | ### 공통 응답 헤더 다음 표는 공통 응답 헤더를 설명합니다. | 헤더 | 설명 | | --- | --- | | Content-Length | 요청 본문의 길이(단위: 바이트) | | Connection | 연결됐는지 여부 | | Date | 요청 메시지의 타임스탬프 | | ETag | 요청 메시지의 MD5 해시값 | | x-amz-request-id | 요청 시 생성된 고유 식별자 | ## 응답 상태 코드 | 오류 코드 | 설명 | HTTP 상태 코드 | | --- | --- | --- | | AccessDenied | 접근이 거부됨 | 403 Forbidden | | BadDigest | 지정한 Content-MD5가 수신한 내용과 일치하지 않음 | 400 Bad Request | | BucketAlreadyExists | 요청한 버킷 이름 사용 불가 해당 버킷의 네임스페이스는 시스템을 이용하는 모든 사용자가 공유함 다른 이름을 선택한 후 다시 시도 필요 | 409 Conflict | | BucketNotEmpty | 삭제하려는 버킷이 비어 있지 않음 | 409 Conflict | | CredentialsNotSupported | 해당 요청은 Credentials을 지원하지 않음 | 400 Bad Request | | EntityTooSmall | 업로드하려는 오브젝트의 크기가 허용되는 최소 크기보다 작음 | 400 Bad Request | | EntityTooLarge | 업로드하려는 오브젝트의 크기가 허용되는 최대 크기 초과 | 400 Bad Request | | IncompleteBody | Content-Length HTTP 헤더에 값을 지정하지 않음 | 400 Bad Request | | IncorrectNumberOfFilesInPostRequest | POST 요청으로는 파일을 하나만 업로드 가능 | 400 Bad Request | | InlineDataTooLarge | 인라인 데이터의 크기가 허용된 최대 크기를 초과 | 400 Bad Request | | InternalError | 내부 오류 발생 재시도 필요 | 500 Internal Server Error | | InvalidAccessKeyId | 입력한 AWS 액세스 키 아이디가 데이터베이스에 존재하지 않음 | 403 Forbidden | | InvalidArgument | 유효하지 않은 파라미터 | 400 Bad Request | | InvalidArgument | 시크릿 키의 MD5 해시값이 잘못됨 MD5 해시는 Base64 인코딩되어야 함 | 400 Bad Request | | InvalidArgument | MD5 해시값이 입력값과 일치하지 않음 | 400 Bad Request | | InvalidBucketName | 지정한 버킷이 유효하지 않음 | 400 Bad Request | | InvalidBucketState | 해당 요청은 현재의 버킷 상태로는 유효하지 않음 | 409 Conflict | | InvalidDigest | 지정한 Content-MD5가 유효하지 않음 | 400 Bad Request | | InvalidEncryptionAlgorithmError | 지정한 Encryption 요청이 유효하지 않음 지원되는 값은 AES256 | 400 Bad Request | | InvalidLocationConstraint | 지역 제약 조건이 유효하지 않음 | 400 Bad Request | | InvalidObjectState | 현재의 오브젝트 상태에서는 해당 오퍼레이션이 유효하지 않음 | 403 Forbidden | | InvalidPart | 지정된 파트 중 하나 이상을 찾을 수 없음 해당 파트가 업로드되지 않았거나 지정된 엔티티 태그가 파트의 엔티티 태그와 일치하지 않을 수 있음 | 400 Bad Request | | InvalidPartOrder | 파트 목록이 오름차순으로 정렬되지 않았음 파트 번호순으로 정렬되어야 함 | 400 Bad Request | | InvalidRange | 요청한 범위를 충족시킬 수 없음 | 416 Requested Range Not Satisfiable | | InvalidRequest | AWS4-HMAC-SHA256 알고리즘 사용 필요 | 400 Bad Request | | InvalidSecurity | 입력한 보안 Credentials 이 유효하지 않음 | 403 Forbidden | | InvalidURI | 지정된 URI의 구문을 분석할 수 없음 | 400 Bad Request | | KeyTooLong | 키가 너무 김 | 400 Bad Request | | MalformedACLError | 입력한 XML 형식이 올바르지 않거나, 게시된 스키마의 유효성을 검사하지 않았음 | 400 Bad Request | | MalformedPOSTRequest | POST 요청 본문의 multipart/form-data 속성값이 올바르지 않음 | 400 Bad Request | | MalformedXML | 설정이 잘못된 XML(XSD 규정을 따르지 않는 XML)을 전송할 때 발생하는 오류 오류 메시지: "The XML you provided was not well-formed or did not validate against our published schema." | 400 Bad Request | | MaxMessageLengthExceeded | 요청이 너무 김 | 400 Bad Request | | MaxPostPreDataLengthExceededError | 업로드 파일 앞의 POST 요청 필드가 너무 큼 | 400 Bad Request | | MetadataTooLarge | 메타데이터의 헤더 크기가 허용된 최대 크기 초과 | 400 Bad Request | | MethodNotAllowed | 지정한 메서드는 이 리소스에 사용 불가 | 405 Method Not Allowed | | MissingContentLength | Content-Length HTTP 헤더 제공 필요 | 411 Length Required | | MissingRequestBodyError | 빈 XML 문서를 전송할 때 발생하는 오류 오류 메시지: "Request body is empty." | 400 Bad Request | | NoSuchBucket | 지정한 버킷이 존재하지 않음 | 404 Not Found | | NoSuchKey | 지정한 키가 존재하지 않음 | 404 Not Found | | NoSuchUpload | 지정한 멀티파트 업로드가 존재하지 않음 업로드 ID가 유효하지 않거나, 멀티파트 업로드가 중단 또는 완료되었을 수 있음 | 404 Not Found | | NotImplemented | 입력한 헤더는 구현되지 않은 기능 의미 | 501 Not Implemented | | OperationAborted | 조건부 연산이 이 리소스와 충돌하여 재시도 필요 | 409 Conflict | | PreconditionFailed | 지정한 전제 조건 중 하나 이상이 수행되지 않음 | 412 Precondition Failed | | Redirect | 임시로 새 위치로 리디렉션함 | 307 Moved Temporarily | | RequestIsNotMultiPartContent | 버킷 POST 요청에는 enclosure-type multipart/form-data 속성값을 지정해야 함 | 400 Bad Request | | RequestTimeout | 소켓 연결이 제한 시간 내에 서버에 도달하지 못했음 | 400 Bad Request | | RequestTimeTooSkewed | 요청 시간과 서버 시간 간의 차이가 너무 큼 | 403 Forbidden | | SignatureDoesNotMatch | 입력한 서명값이 측정된 서명값과 일치하지 않음 AWS 시크릿 키와 서명 방식 확인 필요 | 403 Forbidden | | ServiceUnavailable | 요청 빈도 줄여야 함 | 503 Service Unavailable | | SlowDown | 요청 빈도 줄여야 함 | 503 Slow Down | | TemporaryRedirect | DNS를 업데이트하는 동안 버킷으로 리디렉션 | 307 Moved Temporarily | | TooManyBuckets | 버킷 생성 허용 개수 초과 | 400 Bad Request | | UnexpectedContent | 해당 요청은 콘텐츠를 지원하지 않음 | 400 Bad Request | | UnresolvableGrantByEmailAddress | 입력하신 메일 주소와 일치하는 계정이 데이터베이스에 존재하지 않음 | 400 Bad Request | | UserKeyMustBeSpecified | 버킷 POST 요청은 지정된 필드 이름을 포함해야 함 필드 이름이 지정돼 있다면 필드 순서 확인 필요 | 400 Bad Request | ## 인증 네이버 클라우드 플랫폼 Object Storage API의 인증은 Authorization 헤더를 통해 이루어집니다. 인증 헤더(Authorization)를 생성하는 순서는 다음과 같습니다. [1. 표준화 요청 생성 (Canonical Request)](/docs/storage-objectstorage#1%ED%91%9C%EC%A4%80%ED%99%94-%EC%9A%94%EC%B2%AD-%EC%83%9D%EC%84%B1CanonicalRequest) [2. 서명할 문자열 생성 (String to Sign)](/docs/storage-objectstorage#2%EC%84%9C%EB%AA%85%ED%95%A0-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%83%9D%EC%84%B1StringtoSign) [3. 서명키 생성 (Signing Key)](/docs/storage-objectstorage#3%EC%84%9C%EB%AA%85%ED%82%A4-%EC%83%9D%EC%84%B1SigningKey) [4. 서명 생성 (Signature)](/docs/storage-objectstorage#4%EC%84%9C%EB%AA%85-%EC%83%9D%EC%84%B1Signature) [5. 인증 헤더 생성 (Authorization)](/docs/storage-objectstorage#5%EC%9D%B8%EC%A6%9D-%ED%97%A4%EB%8D%94-%EC%83%9D%EC%84%B1Authorization) ### 1. 표준화 요청 생성 (Canonical Request) 표준화 요청(Canonical Request)의 구성과 각 항목에 대한 설명은 다음과 같습니다. ``` \n \n \n \n \n ``` - `HTTPMethod` 사용하려는 HTTP 메서드를 선언합니다. <예시> PUT - `CanonicalURI` 접근하려는 리소스를 표준화된 방식(URI-encoded)으로 정의합니다. <예시> /path/object 주의 - 객체 키 이름으로 알파벳, 숫자, '-', '_', '~', '.', '/' 문자를 인코딩할 수 없습니다. - 공백문자는 "%20"으로 인코딩되어야 합니다. - `CanonicalQueryString` 요청 파라미터가 있는 경우에는 표준화된 방식(URI-encoded)으로 정의하고 알파벳순으로 나열합니다. ``` UriEncode("marker")+"="+UriEncode("someMarker")+"&"+ UriEncode("max-keys")+"="+UriEncode("20") + "&" + UriEncode("prefix")+"="+UriEncode("somePrefix") ``` - `CanonicalHeaders` 요청에 포함된 헤더 이름과 값을 표준화된 방식으로 정의합니다. 헤더 이름은 모두 소문자로 변환하고, 헤더 값의 앞뒤 공백을 제거합니다. 각 헤더 항목의 끝에 개행 문자`\n`을 삽입합니다. 이 때 헤더 이름으로 알파벳순으로 정렬되어야 합니다. `host` 헤더와 `x-amz-`로 시작하는 헤더는 필수 대상이고, 좀 더 정확한 데이터 정합성 확인을 위해 모든 헤더를 포함시키는 것을 권장합니다. Signature Version 4 인증의 모든 요청에서는 `x-amz-content-sha256` 헤더가 포함되어야 합니다. 데이터(payload)에 대한 해시값을 입력해야 하고 2개의 옵션을 제공합니다. ``` Lowercase()+":"+Trim()+"\n" Lowercase()+":"+Trim()+"\n" ``` - 서명된 데이터(signed payload) : 데이터(payload)의 SHA256 해시의 16진수값을 입력합니다. 만약 데이터(payload)가 없다면 빈 문자열의 해시값을 입력해야 합니다. - 서명되지 않은 데이터(unsigned payload) : 데이터(payload)를 서명하지 않으려면 `x-amz-content-sha256` 헤더의 값으로 `UNSIGNED-PAYLOAD` 문자열을 입력합니다. - `SignedHeaders` 요청에 포함된 헤더 이름을 나열합니다. 헤더 이름은 모두 소문자로 변환하고 알파벳순으로 정렬합니다. 각 헤더 이름의 구분자로 세미콜론(";")을 입력합니다. ``` host;x-amz-content-sha256;x-amz-date ``` - `HashedPayload` 데이터(payload)의 SHA256 해시의 16진수값을 입력합니다. (`x-amz-content-sha256` 헤더의 값과 동일) 만약 요청 데이터(payload)가 없다면 빈 문자열의 해시값을 입력해야 합니다. 데이터(payload)를 서명하지 않으려면 `UNSIGNED-PAYLOAD` 문자열을 입력합니다. ``` Hex(SHA256Hash() or Hex(SHA256Hash("")) or UNSIGNED-PAYLOAD ``` ### 2. 서명할 문자열 생성 (String to Sign) 서명할 문자열(String to Sign)의 구성과 각 항목에 대한 설명은 다음과 같습니다. ``` AWS4-HMAC-SHA256\n \n \n Hex(SHA256Hash()) ``` - `Timestamp` UTC(Coordinated Universal Time)를 기준으로 ISO 8601 표준 규정에 따라 표시되어야 합니다. <예시> 20161128T152924Z - `Scope` Scope 문자열은 다음과 같이 구성됩니다. ``` //s3/aws4_request ``` - Date : `YYYYMMDD` 포맷으로 입력합니다. - Region : 리전 이름을 입력합니다. 리전 이름은 2. 호출 도메인 확인에서 확인할 수 있습니다. 리전 이름은 필수 입력값이지만 네이버 클라우드 플랫폼 Object Storage에서는 도메인으로 리전을 구분하기 때문에 리전 이름의 정합성을 확인하지는 않습니다. - `CanonicalRequest` 생성한 표준화 요청(Canonical Request)의 SHA256 해시의 16진수값을 입력합니다. ### 3. 서명키 생성 (Signing Key) 서명키(Signing Key)의 구상과 각 항목에 대한 설명은 다음과 같습니다. ``` kSecret = kDate = HMAC-SHA256("AWS4" + kSecret, ) kRegion = HMAC-SHA256(kDate, ) kService = HMAC-SHA256(kRegion, "s3") kSigning = HMAC-SHA256(kService, "aws4_request") ``` - `SecretKey` API 인증키 중 Secret Key를 입력합니다. 네이버 클라우드 플랫폼 콘솔의 **My Account** > **계정 및 보안 관리** > **보안 관리** > **접근 관리** 메뉴에서 확인할 수 있습니다. - `Date` `YYYYMMDD` 포맷으로 입력합니다. - `Region` 리전 이름을 입력합니다. 리전 이름은 2. 호출 도메인 확인에서 확인할 수 있습니다. 리전 이름은 필수 입력값이지만 네이버 클라우드 플랫폼 Object Storage에서는 도메인으로 리전을 구분하기 때문에 리전 이름의 정합성을 확인하지는 않습니다. ### 4. 서명 생성 (Signature) 서명(Signature)의 구성과 각 항목에 대한 설명은 다음과 같습니다. ``` Hex(HMAC-SHA256(, )) ``` - `Signing Key` 서명키의 HMAC-SHA256 해시를 계산하여 16진수값으로 표시합니다. - `StringToSign` 서명할 문자열의 HMAC-SHA256 해시를 계산하여 16진수값으로 표시합니다. ### 5. 인증 헤더 생성 (Authorization) 인증 헤더(Authorization)의 구성과 각 항목에 대한 설명은 다음과 같습니다. ``` Authorization: AWS4-HMAC-SHA256 Credential=/, SignedHeaders=, Signature= ``` - `AccessKeyID` API 인증키 중 Access Key ID를 입력합니다. 네이버 클라우드 플랫폼 콘솔의 **My Account** > **계정 및 보안 관리** > **보안 관리** > **접근 관리** 메뉴에서 확인할 수 있습니다. - `Scope` Scope 문자열은 다음과 같이 구성됩니다. ``` //s3/aws4_request ``` - Date : `YYYYMMDD` 포맷으로 입력합니다. - Region : 리전 이름을 입력합니다. 리전 이름은 2. 호출 도메인 확인에서 확인할 수 있습니다. 리전 이름은 필수 입력값이지만 네이버 클라우드 플랫폼 Object Storage에서는 도메인으로 리전을 구분하기 때문에 리전 이름의 정합성을 확인하지는 않습니다. - `SignedHeaders` 요청에 포함된 헤더 이름을 나열합니다. 헤더 이름은 모두 소문자로 변환하고 알파벳순으로 정렬합니다. 각 헤더 이름의 구분자로 세미콜론(";")을 입력합니다. ``` host;x-amz-content-sha256;x-amz-date ``` - `Signature` 4)서명 생성(Signature)에서 생성한 서명(Signature)을 입력합니다. 언어별 인증 헤더(Authorization) 생성 예시는 다음과 같습니다. ``` public class ObjectStorageSample { private static byte[] sign(String stringData, byte[] key) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException { byte[] data = stringData.getBytes(CHARSET_NAME); Mac e = Mac.getInstance(HMAC_ALGORITHM); e.init(new SecretKeySpec(key, HMAC_ALGORITHM)); return e.doFinal(data); } private static String hash(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException { MessageDigest e = MessageDigest.getInstance(HASH_ALGORITHM); e.update(text.getBytes(CHARSET_NAME)); return Hex.encodeHexString(e.digest()); } private static String getStandardizedQueryParameters(String queryString) throws UnsupportedEncodingException { TreeMap sortedQueryParameters = new TreeMap<>(); // sort by key name if (queryString != null && !queryString.isEmpty()) { String[] queryStringTokens = queryString.split("&"); for (String field : queryStringTokens) { String[] fieldTokens = field.split("="); if (fieldTokens.length > 0) { if (fieldTokens.length > 1) { sortedQueryParameters.put(fieldTokens[0], fieldTokens[1]); } else { sortedQueryParameters.put(fieldTokens[0], ""); } } } } StringBuilder standardizedQueryParametersBuilder = new StringBuilder(); int count = 0; for (String key : sortedQueryParameters.keySet()) { if (count > 0) { standardizedQueryParametersBuilder.append("&"); } standardizedQueryParametersBuilder.append(key).append("="); if (sortedQueryParameters.get(key) != null && !sortedQueryParameters.get(key).isEmpty()) { standardizedQueryParametersBuilder.append(URLEncoder.encode(sortedQueryParameters.get(key), CHARSET_NAME)); } count++; } return standardizedQueryParametersBuilder.toString(); } private static TreeMap getSortedHeaders(Header[] headers) { TreeMap sortedHeaders = new TreeMap<>(); // sort by header name for (Header header : headers) { String headerName = header.getName().toLowerCase(); sortedHeaders.put(headerName, header.getValue()); } return sortedHeaders; } private static String getSignedHeaders(TreeMap sortedHeaders) { StringBuilder signedHeadersBuilder = new StringBuilder(); for (String headerName : sortedHeaders.keySet()) { signedHeadersBuilder.append(headerName.toLowerCase()).append(";"); } String s = signedHeadersBuilder.toString(); if (s.endsWith(";")) { s = s.substring(0, s.length() - 1); } return s; } private static String getStandardizedHeaders(TreeMap sortedHeaders) { StringBuilder standardizedHeadersBuilder = new StringBuilder(); for (String headerName : sortedHeaders.keySet()) { standardizedHeadersBuilder.append(headerName.toLowerCase()).append(":").append(sortedHeaders.get(headerName)).append("\n"); } return standardizedHeadersBuilder.toString(); } private static String getCanonicalRequest(HttpUriRequest request, String standardizedQueryParameters, String standardizedHeaders, String signedHeaders) { StringBuilder canonicalRequestBuilder = new StringBuilder().append(request.getMethod()).append("\n") .append(request.getURI().getPath()).append("\n") .append(standardizedQueryParameters).append("\n") .append(standardizedHeaders).append("\n") .append(signedHeaders).append("\n") .append(UNSIGNED_PAYLOAD); return canonicalRequestBuilder.toString(); } private static String getScope(String datestamp, String regionName) { StringBuilder scopeBuilder = new StringBuilder().append(datestamp).append("/") .append(regionName).append("/") .append(SERVICE_NAME).append("/") .append(REQUEST_TYPE); return scopeBuilder.toString(); } private static String getStringToSign(String timestamp, String scope, String canonicalRequest) throws NoSuchAlgorithmException, UnsupportedEncodingException { StringBuilder stringToSignBuilder = new StringBuilder(AWS_ALGORITHM) .append("\n") .append(timestamp).append("\n") .append(scope).append("\n") .append(hash(canonicalRequest)); return stringToSignBuilder.toString(); } private static String getSignature(String secretKey, String datestamp, String regionName, String stringToSign) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException { byte[] kSecret = ("AWS4" + secretKey).getBytes(CHARSET_NAME); byte[] kDate = sign(datestamp, kSecret); byte[] kRegion = sign(regionName, kDate); byte[] kService = sign(SERVICE_NAME, kRegion); byte[] signingKey = sign(REQUEST_TYPE, kService); return Hex.encodeHexString(sign(stringToSign, signingKey)); } private static String getAuthorization(String accessKey, String scope, String signedHeaders, String signature) { String signingCredentials = accessKey + "/" + scope; String credential = "Credential=" + signingCredentials; String signerHeaders = "SignedHeaders=" + signedHeaders; String signatureHeader = "Signature=" + signature; StringBuilder authHeaderBuilder = new StringBuilder().append(AWS_ALGORITHM).append(" ") .append(credential).append(", ") .append(signerHeaders).append(", ") .append(signatureHeader); return authHeaderBuilder.toString(); } private static void authorization(HttpUriRequest request, String regionName, String accessKey, String secretKey) throws Exception { Date now = new Date(); DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); TIME_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); String datestamp = DATE_FORMATTER.format(now); String timestamp = TIME_FORMATTER.format(now); request.addHeader("X-Amz-Date", timestamp); request.addHeader("X-Amz-Content-Sha256", UNSIGNED_PAYLOAD); String standardizedQueryParameters = getStandardizedQueryParameters(request.getURI().getQuery()); TreeMap sortedHeaders = getSortedHeaders(request.getAllHeaders()); String signedHeaders = getSignedHeaders(sortedHeaders); String standardizedHeaders = getStandardizedHeaders(sortedHeaders); String canonicalRequest = getCanonicalRequest(request, standardizedQueryParameters, standardizedHeaders, signedHeaders); System.out.println("> canonicalRequest :"); System.out.println(canonicalRequest); String scope = getScope(datestamp, regionName); String stringToSign = getStringToSign(timestamp, scope, canonicalRequest); System.out.println("> stringToSign :"); System.out.println(stringToSign); String signature = getSignature(secretKey, datestamp, regionName, stringToSign); String authorization = getAuthorization(accessKey, scope, signedHeaders, signature); request.addHeader("Authorization", authorization); } private static void putObject(String bucketName, String objectName, String localFilePath) throws Exception { HttpClient httpClient = HttpClientBuilder.create().build(); HttpPut request = new HttpPut(ENDPOINT + "/" + bucketName + "/" + objectName); request.addHeader("Host", request.getURI().getHost()); request.setEntity(new FileEntity(new File(localFilePath))); authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY); HttpResponse response = httpClient.execute(request); System.out.println("Response : " + response.getStatusLine()); } private static void getObject(String bucketName, String objectName, String localFilePath) throws Exception { HttpClient httpClient = HttpClientBuilder.create().build(); HttpGet request = new HttpGet(ENDPOINT + "/" + bucketName + "/" + objectName); request.addHeader("Host", request.getURI().getHost()); authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY); HttpResponse response = httpClient.execute(request); System.out.println("Response : " + response.getStatusLine()); InputStream is = response.getEntity().getContent(); File targetFile = new File(localFilePath); OutputStream os = new FileOutputStream(targetFile); byte[] buffer = new byte[8 * 1024]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } is.close(); os.close(); } private static void listObjects(String bucketName, String queryString) throws Exception { HttpClient httpClient = HttpClientBuilder.create().build(); URI uri = new URI(ENDPOINT + "/" + bucketName + "?" + queryString); HttpGet request = new HttpGet(uri); request.addHeader("Host", request.getURI().getHost()); authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY); HttpResponse response = httpClient.execute(request); System.out.println("> Response : " + response.getStatusLine()); int i; InputStream is = response.getEntity().getContent(); StringBuffer buffer = new StringBuffer(); byte[] b = new byte[4096]; while ((i = is.read(b)) != -1) { buffer.append(new String(b, 0, i)); } System.out.println(buffer.toString()); } private static final String CHARSET_NAME = "UTF-8"; private static final String HMAC_ALGORITHM = "HmacSHA256"; private static final String HASH_ALGORITHM = "SHA-256"; private static final String AWS_ALGORITHM = "AWS4-HMAC-SHA256"; private static final String SERVICE_NAME = "s3"; private static final String REQUEST_TYPE = "aws4_request"; private static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"; private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd"); private static final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("yyyyMMdd\'T\'HHmmss\'Z\'"); private static final String REGION_NAME = "gov-standard"; private static final String ENDPOINT = "https://kr.object.gov-ncloudstorage.com"; private static final String ACCESS_KEY = "ACCESS_KEY_ID"; private static final String SECRET_KEY = "SECRET_KEY"; public static void main(String[] args) throws Exception { String bucketName = "sample-bucket"; String objectName = "sample-object.txt"; String sourceFilePath = "/tmp/source.txt"; String targetFilePath = "/tmp/target.txt"; putObject(bucketName, objectName, sourceFilePath); getObject(bucketName, objectName, targetFilePath); String queryString = "max-keys=10&delimiter=/"; listObjects(bucketName, queryString); } } ``` ``` import hashlib import hmac import datetime import requests import urllib def get_hash(key, msg): return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() def create_signed_headers(headers): signed_headers = [] for k in sorted(headers): signed_headers.append('%s;' % k) return ''.join(signed_headers)[:-1] def create_standardized_headers(headers): signed_headers = [] for k in sorted(headers): signed_headers.append('%s:%s\n' % (k, headers[k])) return ''.join(signed_headers) def create_standardized_query_parameters(request_parameters): standardized_query_parameters = [] if request_parameters: for k in sorted(request_parameters): standardized_query_parameters.append('%s=%s' % (k, urllib.quote(request_parameters[k], safe=''))) return '&'.join(standardized_query_parameters) else: return '' class ObjectStorageSample: def __init__(self): self.region = 'gov-standard' self.endpoint = 'https://kr.object.gov-ncloudstorage.com' self.host = 'kr.object.gov-ncloudstorage.com' self.access_key = 'ACCESS_KEY_ID' self.secret_key = 'SECRET_KEY' self.payload_hash = 'UNSIGNED-PAYLOAD' self.hashing_algorithm = 'AWS4-HMAC-SHA256' self.service_name = 's3' self.request_type = 'aws4_request' self.time_format = '%Y%m%dT%H%M%SZ' self.date_format = '%Y%m%d' def _create_credential_scope(self, date_stamp): return date_stamp + '/' + self.region + '/' + self.service_name + '/' + self.request_type def _create_canonical_request(self, http_method, request_path, request_parameters, headers): standardized_query_parameters = create_standardized_query_parameters(request_parameters) standardized_headers = create_standardized_headers(headers) signed_headers = create_signed_headers(headers) canonical_request = (http_method + '\n' + request_path + '\n' + standardized_query_parameters + '\n' + standardized_headers + '\n' + signed_headers + '\n' + self.payload_hash) print('canonical_request:\n%s\n' % canonical_request) return canonical_request def _create_string_to_sign(self, time_stamp, credential_scope, canonical_request): string_to_sign = (self.hashing_algorithm + '\n' + time_stamp + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()) print('string_to_sign:\n%s\n' % string_to_sign) return string_to_sign def _create_signature_key(self, date_stamp): key_date = get_hash(('AWS4' + self.secret_key).encode('utf-8'), date_stamp) key_string = get_hash(key_date, self.region) key_service = get_hash(key_string, self.service_name) key_signing = get_hash(key_service, self.request_type) return key_signing def _create_authorization_header(self, headers, signature_key, string_to_sign, credential_scope): signed_headers = create_signed_headers(headers) signature = hmac.new(signature_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest() return (self.hashing_algorithm + ' ' + 'Credential=' + self.access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature) def _sign(self, http_method, request_path, headers, time, request_parameters=None): time_stamp = time.strftime(self.time_format) date_stamp = time.strftime(self.date_format) credential_scope = self._create_credential_scope(date_stamp) canonical_request = self._create_canonical_request(http_method, request_path, request_parameters, headers) string_to_sign = self._create_string_to_sign(time_stamp, credential_scope, canonical_request) signature_key = self._create_signature_key(date_stamp) headers['authorization'] = self._create_authorization_header(headers, signature_key, string_to_sign, credential_scope) def put_object(self, bucket_name, object_name, source_file_path, request_parameters=None): http_method = 'PUT' with open(source_file_path) as f: time = datetime.datetime.utcnow() time_stamp = time.strftime(self.time_format) headers = {'x-amz-date': time_stamp, 'x-amz-content-sha256': self.payload_hash, 'host': self.host} request_path = '/%s/%s' % (bucket_name, object_name) self._sign(http_method, request_path, headers, time, request_parameters) request_url = self.endpoint + request_path r = requests.put(request_url, headers=headers, params=request_parameters, data=f.read()) print('Response code: %d' % r.status_code) def get_object(self, bucket_name, object_name, target_file_path, request_parameters=None): http_method = 'GET' time = datetime.datetime.utcnow() time_stamp = time.strftime(self.time_format) headers = {'x-amz-date': time_stamp, 'x-amz-content-sha256': self.payload_hash, 'host': self.host} request_path = '/%s/%s' % (bucket_name, object_name) self._sign(http_method, request_path, headers, time, request_parameters) request_url = self.endpoint + request_path r = requests.get(request_url, headers=headers, params=request_parameters, stream=True) print('Response code: %d' % r.status_code) if r.status_code == 200: with open(target_file_path, 'wb') as f: f.write(r.content) def list_objects(self, bucket_name, request_parameters=None): http_method = 'GET' time = datetime.datetime.utcnow() time_stamp = time.strftime(self.time_format) headers = {'x-amz-date': time_stamp, 'x-amz-content-sha256': self.payload_hash, 'host': self.host} request_path = '/%s' % bucket_name self._sign(http_method, request_path, headers, time, request_parameters) request_url = self.endpoint + request_path r = requests.get(request_url, headers=headers, params=request_parameters) print('Response code: %d' % r.status_code) print('Response content:\n%s' % r.content) if __name__ == '__main__': sample = ObjectStorageSample() sample.put_object('sample-bucket', 'sample-object.txt', '/tmp/source.txt') sample.get_object('sample-bucket', 'sample-object.txt', '/tmp/target.txt') sample.list_objects('sample-bucket', request_parameters={'max-keys': '10', 'delimiter': '/'}) ``` ## 접근 제어 목록(ACL) 네이버 클라우드 플랫폼 Object Storage 서비스를 이용 중인 회원을 대상으로 버킷과 오브젝트에 접근 권한을 부여할 수 있습니다. 네이버 클라우드 플랫폼 Object Storage 이용신청을 완료한 회원은 Object Storage에서 사용할 수 있는 ID가 발급됩니다. 이 ID는 버킷과 오브젝트의 접근 권한을 설정할 때 사용됩니다. 또한 인증없이 접근 가능하도록 public-read, public-write 권한 설정도 가능합니다. 다음 표는 권한 종류와 해당 권한이 부여된 경우 가능한 오퍼레이션을 설명합니다. | 권한 | 버킷에 부여된 경우 | 오브젝트에 부여된 경우 | | --- | --- | --- | | READ | 버킷에 속한 오브젝트 목록 조회 가능 | 오브젝트 데이터와 메타데이터의 읽기가 가능 | | WRITE | 새 오브젝트를 생성하거나 기존 오브젝트의 덮어쓰기 또는 삭제 가능 | N/A | | READ_ACP | 버킷의 ACL 조회 가능 | 오브젝트의 ACL 조회 가능 | | WRITE_ACP | 버킷의 ACL 설정 가능 | 오브젝트의 ACL 설정 가능 | | FULL_CONTROL | READ, WRITE, READ_ACP, WRITE_ACP 권한으로 가능한 모든 오퍼레이션 수행 가능 | READ, READ_ACP, WRITE_ACP 권한으로 가능한 모든 오퍼레이션 수행 가능 | 다음 표는 NCP 오브젝트 스토리지에서 지원하는 기본 ACL을 설명합니다. 이 표에 언급되지 않은 값은 지원되지 않습니다. | 기본 ACL | 적용 대상 | 설명 | | --- | --- | --- | | private | 버킷, 오브젝트 | 소유자에게 FULL_CONTROL 권한 부여 (default) | | public-read | 버킷, 오브젝트 | 소유자에게 FULL_CONTROL 권한을 부여하고, 모든 사용자에게 READ 권한 부여 | | public-read-write | 버킷, 오브젝트 | 소유자에게 FULL_CONTROL 권한을 부여하고, 모든 사용자에게 READ와 WRITE 권한 부여 | | authenticated-read | 버킷, 오브젝트 | 소유자에게 FULL_CONTROL 권한을 부여하고, 인증된 사용자에게 READ 권한 부여 | public-read를 포함하여 READ 권한이 버킷에 적용된 경우에는 오브젝트를 조회만 할 수 있을 뿐 오브젝트 자체에는 접근할 수 없습니다.