네이버 클라우드 플랫폼 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) | 계정에 속한 버킷 목록 조회 |
버킷 오퍼레이션
버킷을 생성, 삭제, 조회, 제어하는 오퍼레이션을 설명합니다.
버킷 내의 오브젝트 목록 조회 GET Bucket (List Objects) Version 2 는 지원하지 않습니다.
| 오퍼레이션 | 설명 |
|---|---|
| PUT Bucket | 버킷 생성 계정의 버킷 개수는 1,000 개로 제한됨 |
| GET Bucket(List Objects) | 버킷에 포함된 오브젝트 조회 한 번에 최대 1,000 개의 오브젝트 나열 |
| HEAD Bucket | 버킷 헤더 조회 |
| DELETE Bucket | 빈 버킷 삭제 |
| PUT Bucket ACL | 버킷에 적용할 접근 제어 목록(ACL) 생성 |
| GET Bucket ACL | 버킷에 적용된 접근 제어 목록(ACL) 조회 |
| PUT Bucket CORS | 버킷에 적용할 CORS 설정 생성 |
| GET Bucket CORS | 버킷에 적용된 CORS 설정 조회 |
| DELETE Bucket CORS | 버킷에 적용된 CORS 설정 삭제 |
| List Multipart Uploads | 완료되지 않았거나 취소된 멀티파트 업로드 조회 |
오브젝트 오퍼레이션
오브젝트를 생성, 삭제, 조회, 제어하는 오퍼레이션을 설명합니다.
| 오퍼레이션 | 설명 |
|---|---|
| PUT Object | 버킷에 오브젝트 추가(업로드) |
| PUT Object (Copy) | 오브젝트 복사본 생성 |
| GET Object | 오브젝트 조회(다운로드) |
| HEAD Object | 오브젝트 헤더 조회 |
| DELETE Object | 버킷에서 오브젝트 삭제 |
| DELETE Multiple Objects | 버킷에서 멀티파트 오브젝트 삭제 |
| PUT Object ACL | 오브젝트에 적용할 접근 제어 목록(ACL) 생성 |
| GET Object ACL | 오브젝트에 적용된 접근 제어 목록(ACL) 조회 |
| OPTIONS Object | CORS 설정을 확인하여 특정 요청을 전송할 수 있는지 조회 |
| Initiate Multipart Upload | 업로드할 파트 집합에 지정할 업로드 ID 생성 |
| Upload Part | 업로드 ID와 연관된 오브젝트의 파트 업로드 |
| Complete Multipart Upload | 업로드 ID와 연관된 파트로 분리된 오브젝트 결합 |
| Abort Multipart Upload | 업로드를 중단하고 업로드 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)
2. 서명할 문자열 생성 (String to Sign)
3. 서명키 생성 (Signing Key)
4. 서명 생성 (Signature)
5. 인증 헤더 생성 (Authorization)
1. 표준화 요청 생성 (Canonical Request)
표준화 요청(Canonical Request)의 구성과 각 항목에 대한 설명은 다음과 같습니다.
<HTTPMethod>\n
<CanonicalURI>\n
<CanonicalQueryString>\n
<CanonicalHeaders>\n
<SignedHeaders>\n
<HashedPayload>
-
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개의 옵션을 제공합니다.- 서명된 데이터(signed payload) : 데이터(payload)의 SHA256 해시의 16진수값을 입력합니다. 만약 데이터(payload)가 없다면 빈 문자열의 해시값을 입력해야 합니다.
- 서명되지 않은 데이터(unsigned payload) : 데이터(payload)를 서명하지 않으려면
x-amz-content-sha256헤더의 값으로UNSIGNED-PAYLOAD문자열을 입력합니다.
Lowercase(<HeaderName1>)+":"+Trim(<value>)+"\n" Lowercase(<HeaderName2>)+":"+Trim(<value>)+"\n" -
SignedHeaders
요청에 포함된 헤더 이름을 나열합니다. 헤더 이름은 모두 소문자로 변환하고 알파벳순으로 정렬합니다. 각 헤더 이름의 구분자로 세미콜론(";")을 입력합니다.host;x-amz-content-sha256;x-amz-date -
HashedPayload
데이터(payload)의 SHA256 해시의 16진수값을 입력합니다. (x-amz-content-sha256헤더의 값과 동일) 만약 요청 데이터(payload)가 없다면 빈 문자열의 해시값을 입력해야 합니다. 데이터(payload)를 서명하지 않으려면UNSIGNED-PAYLOAD문자열을 입력합니다.Hex(SHA256Hash(<payload>) or Hex(SHA256Hash("")) or UNSIGNED-PAYLOAD
2. 서명할 문자열 생성 (String to Sign)
서명할 문자열(String to Sign)의 구성과 각 항목에 대한 설명은 다음과 같습니다.
AWS4-HMAC-SHA256\n
<Timestamp>\n
<Scope>\n
Hex(SHA256Hash(<CanonicalRequest>))
-
Timestamp
UTC(Coordinated Universal Time)를 기준으로 ISO 8601 표준 규정에 따라 표시되어야 합니다. <예시> 20161128T152924Z -
Scope
Scope 문자열은 다음과 같이 구성됩니다.<Date>/<Region>/s3/aws4_request- Date :
YYYYMMDD포맷으로 입력합니다. - Region : 리전 이름을 입력합니다. 리전 이름은 2. 호출 도메인 확인에서 확인할 수 있습니다. 리전 이름은 필수 입력값이지만 네이버 클라우드 플랫폼 Object Storage에서는 도메인으로 리전을 구분하기 때문에 리전 이름의 정합성을 확인하지는 않습니다.
- Date :
-
CanonicalRequest
생성한 표준화 요청(Canonical Request)의 SHA256 해시의 16진수값을 입력합니다.
3. 서명키 생성 (Signing Key)
서명키(Signing Key)의 구상과 각 항목에 대한 설명은 다음과 같습니다.
kSecret = <SecretKey>
kDate = HMAC-SHA256("AWS4" + kSecret, <Date>)
kRegion = HMAC-SHA256(kDate, <Region>)
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(<SigningKey>, <StringToSign>))
-
Signing Key
서명키의 HMAC-SHA256 해시를 계산하여 16진수값으로 표시합니다. -
StringToSign
서명할 문자열의 HMAC-SHA256 해시를 계산하여 16진수값으로 표시합니다.
5. 인증 헤더 생성 (Authorization)
인증 헤더(Authorization)의 구성과 각 항목에 대한 설명은 다음과 같습니다.
Authorization: AWS4-HMAC-SHA256 Credential=<AccessKeyID>/<Scope>, SignedHeaders=<SignedHeaders>, Signature=<Signature>
-
AccessKeyID
API 인증키 중 Access Key ID를 입력합니다. 네이버 클라우드 플랫폼 콘솔의 My Account > 계정 및 보안 관리 > 보안 관리 > 접근 관리 메뉴에서 확인할 수 있습니다. -
Scope
Scope 문자열은 다음과 같이 구성됩니다.<Date>/<Region>/s3/aws4_request- Date :
YYYYMMDD포맷으로 입력합니다. - Region : 리전 이름을 입력합니다. 리전 이름은 2. 호출 도메인 확인에서 확인할 수 있습니다. 리전 이름은 필수 입력값이지만 네이버 클라우드 플랫폼 Object Storage에서는 도메인으로 리전을 구분하기 때문에 리전 이름의 정합성을 확인하지는 않습니다.
- Date :
-
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<String, String> 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<String, String> getSortedHeaders(Header[] headers) {
TreeMap<String, String> 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<String, String> 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<String, String> 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<String, String> 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 권한이 버킷에 적용된 경우에는 오브젝트를 조회만 할 수 있을 뿐 오브젝트 자체에는 접근할 수 없습니다.