인증 관리
S3 API를 사용하여 네이버 클라우드 플랫폼 Object Storage에서 생성되는 요청은 AWS의 authorization 헤더 구현체를 사용하여 인증되어야 합니다. 네이버 클라우드 플랫폼 Object Storage는 Signature Version 2와 Signature Version 4 인증 방식을 지원합니다. Signature Version 4 인증 방식은 서명 인증 단계에서 접근용 시크릿 키가 아닌 파생된 키를 사용하고 있어 좀 더 안전합니다. 또한 요청에 포함된 타임스탬프와 해당 서명이 연결되어 있어 인증 헤더를 재사용할 수 없기 때문에 전송되는 데이터의 무결성을 보장받을 수 있습니다. 헤더는 ‘알고리즘’, ‘Credential 정보(scope)’, ‘서명된 헤더 목록’, ‘서명’의 네 가지 요소로 구성되어 있습니다.
AWS4-HMAC-SHA256 Credential={access-key}/{date}/{region}/s3/aws4_request,SignedHeaders=host;x-amz-date;{other-required-headers},Signature={signature}
날짜는 YYYYMMDD 형식으로 표시되고, 리전은 네이버 클라우드 플랫폼 Object Storage API 소개 > 호출 도메인(Endpoint)에 따라 입력합니다. host 및 x-amz-date 헤더는 항상 필수이며, 요청에 따라 다른 헤더가 필요할 수 있습니다(예: 페이로드를 포함한 요청인 경우 x-amz-content-sha256 필요). 개별 요청마다 서명을 다시 계산해야 하기 때문에 대부분의 개발자가 인증 헤더를 자동으로 생성하는 도구나 SDK를 사용하는 것을 권장합니다. authorization 헤더를 생성하기 전에 먼저 표준화 형식으로 요청을 작성해야 합니다.
- 사용하려는 HTTP 메서드를 선언합니다(예: PUT).
- 접근하려는 리소스를 표준화된 방식으로 정의합니다. 이는 http(s)://와 쿼리 문자열 사이의 주소를 작성하는 단계입니다. 계정 레벨의 요청(예: 버킷 조회)인 경우 주소 다음에 / 문자만 추가하면 됩니다.
- 요청 파라미터가 있는 경우에는 표준 방식으로 URL을 인코딩(예: 공백 자리에 % 문자를 사용해 %20)과 같이 표시)하고 알파벳순으로 나열합니다.
- 헤더에서는 공백을 제거하고, 대문자는 소문자로 변환하고, 각 헤더 항목의 끝에 개행 문자를 삽입합니다. 이때 헤더 목록은 ASCII순으로 정렬되어야 합니다.
- 표준 형식으로 나열된 헤더에 서명을 추가합니다. 서명에는 값이 아닌 헤더 이름이 포함됩니다. 알파벳순으로 나열하고 세미콜론으로 구분한 헤더 이름을 나열합니다. 모든 요청에는 반드시 host 및 x-amz-date가 포함되어야 합니다.
- 오브젝트를 업로드하거나 ACL을 생성하는 요청에 본문이 있는 경우 요청 본문은 SHA-256 알고리즘을 사용하여 해시되어야 하고 Base16으로 인코딩된 소문자로 표시되어야 합니다.
- HTTP 메서드, 표준 리소스, 표준 파라미터, 표준 헤더, 서명된 헤더, 해시된 요청 본문을 결합합니다. 이때 각 항목의 끝에는 개행 문자를 삽입합니다.
다음에서는 "서명할 문자열"을 만들고 이를 서명 키와 결합하여 서명을 확정하는 방법을 설명합니다. "서명할 문자열"은 다음과 같은 형식으로 구성됩니다.
AWS4-HMAC-SHA256
{time}
{date}/{string}/s3/aws4_request
{hashed-standardized-request}
- 시간은 UTC(Coordinated Universal Time)를 기준으로 ISO 8601 표준 규정에 따라 표시되어야 합니다(예: 20161128T152924Z).
- 날짜 형식은 YYYYMMDD입니다.
- 마지막 줄은 SHA-256 알고리즘을 사용하여 직전에 생성한 표준 요청 해시를 나타냅니다.
다음에서는 서명을 계산하는 방법을 설명합니다.
- 먼저 해당 계정의 시크릿 키, 현재 날짜, 리전, 사용 중인 API 타입 정보를 기반으로 생성된 서명 키가 있어야 합니다.
- AWS4 문자열이 시크릿 키 앞에 추가됩니다. 해당 문자열은 날짜를 해시하는 키로 사용됩니다.
- 생성된 해시값은 리전을 해시하는 키로 사용됩니다.
- 이와 같은 프로세스는 API 타입을 해시하기 위한 키로 사용될 해시값이 생성될 때까지 계속됩니다.
- 마지막에 생성된 해시값은 서명 키를 생성하는 aws4_request 문자열을 해시하는 키로 사용됩니다.
- 생성된 서명 키는 최종 서명을 생성할 ‘서명할 문자열’을 해시하는 키로 사용됩니다.
마지막 단계는 아래 예와 같이 각 정보를 조합하여 authorization 헤더를 생성하는 것입니다.
AWS4-HMAC-SHA256 Credential={access-key}/{date}/{region}/s3/aws4_request,SignedHeaders=host;x-amz-date;{other-required-headers},Signature={signature}
authorization 헤더 생성 예 (Java)
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) {
sortedHeaders.put(header.getName(), 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(";");
}
return signedHeadersBuilder.toString();
}
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 = "kr-standard";
private static final String ENDPOINT = "https://kr.object.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);
}
}
authorization 헤더 생성 예 (Python2.7)
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)
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 = 'kr-standard'
self.endpoint = 'https://kr.object.ncloudstorage.com'
self.host = 'kr.object.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': '/'})