AWS CloudFront로 비공개 컨텐츠 혹은 유료 컨텐츠를 서빙할 때는 Signed URL/Cookie 라는 토큰 기반의 인증 옵션을 사용한다.
1. 구성은 뭐 인증 API 서버가 필요할테고 일반적인 인증 구성
2. CloudFront Signed URL/Cookie를 위한 Key Pair 생성
[Root 계정 로그인] > [우측 계정명] > [My Security Credentials] > [CloudFront Key Pairs] > [Create New Key Pair]
Private, Public Key 그리고 Key Pair ID를 저장
3. CloudFront Signed URL/Cookie 미리 준비된 정책 vs 사용자 정책
둘의 차이는 아래 그림과 같다. 그냥 사용자 지정 정책을 사용하자.
사용자 정책을 만들면 인증 시간, Client IP 제어가 가능하다. Policy 변수만 추가하면 사용자 지정 정책이 된다. 사실 AWS에서 굳이 이걸 왜 나눠놨는지 모르겠다. 오히려 헷갈린다.
Client IP 제어 사용 시 주의점은 IPv6는 지원되지 않는다. 따라서 범용적인 유료서비스의 경우 Client IP 제어는 사용하지 말자.
4. CloudFront Singed URL vs Cookie
Signed URL의 경우 일반적인 컨텐츠 및 RTMP 서비스 시 권장한다. Signed Cookie의 경우 DASH, HLS와 같은 manifest file을 사용하는 미디어 HTTP 프로토콜(?) 서비스 시 권장한다.
Signed URL의 경우 Expires, Signature, Key-Pair-Id 등이 기존 도메인에 추가되어 Signed URL을 생성한다.
e.g 서비스 URL
http://d1e5lqevy0hhfg.cloudfront.net/test.mp
http://d1e5lqevy0hhfg.cloudfront.net/test.mp
미리 준비된 정책 Signed URL
http://d1e5lqevy0hhfg.cloudfront.net/test.mp4?Expires=1488263708&Signature=eRGDtIzaj-a~T~xf8nleM2vpdjaqya~vzFC8m5elA-NBU6i6WVSPv9phnaN5rfuWpIJt~zqSnzDY4CLbjmWJbGt24BEv2zi-m-zjkNDzYQ0vDhGr3zu5R8U~oA4K8M~CG4ynN8CfOE8B7-fI4sCSzxc8HBo62sD9s1nXKHXErOuBcs-GoVpgW6ktxwNoTSSzXNWOSv1mA47DFAb~8Ln6nu8LbCcDYjX94T7GVqERsMr0xuMS6axnAuaSc8~62ZXb0e8YOgkLcWW6vn8ZmYAJI4NVlmuAmA01pmc9gSh-rzvlKL2VJx9gseSklUpstwf1gEmbTf~zuHOoMgne~gBo6Q__&Key-Pair-Id=APKAICJI44KY7SDYRFQQ
사용자 정책 Signed URL
http://d1e5lqevy0hhfg.cloudfront.net/test.mp4?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2QxZTVscWV2eTBoaGZnLmNsb3VkZnJvbnQubmV0L3Rlc3QubXA0IiwiQ29uZGl0aW9uIjp7IklwQWRkcmVzcyI6eyJBV1M6U291cmNlSXAiOiIyMTguMjM2Ljg0LjQzXC8yNCJ9LCJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTQ4ODI2MzU5NX19fV19&Signature=CMrBVA7b~j91UvI9XLm5MeOfFiPfLX-AncEepvTc9g~ZE8DEZuHYlkm5HzBq5hGNjvzdiHu-sZM6ZUXx0hmkkeyfw-L6pWcz0KD58k~X3h9MreLUswmNhoSsrTnL-5njmboYUyScmenF-pl17lZee-4pJJG-tNcEktVCwo9QGC1rDrv1sEbn9mqyb9UNXa3bqp74tk8b~6palnPPQM9rO2-Uj~fws6lbaEFii5mT7gzua87hSLc6NpNm0C8mUL6oDg9q5cmNcanGOFlpPMRX8-tEgvuXML7m6TBuZH8etnhanZaFa0-fogKnr~s4y4jpkAKVpMPMa56m7zNrdbW4cA__&Key-Pair-Id=APKAICJI44KY7SDYRFQQ
디코딩
echo -n "eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2QxZTVscWV2eTBoaGZnLmNsb3VkZnJvbnQubmV0L3Rlc3QubXA0IiwiQ29uZGl0aW9uIjp7IklwQWRkcmVzcyI6eyJBV1M6U291cmNlSXAiOiIyMTguMjM2Ljg0LjQzXC8yNCJ9LCJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTQ4ODI2MzU5NX19fV19&Signature=CMrBVA7b~j91UvI9XLm5MeOfFiPfLX-AncEepvTc9g~ZE8DEZuHYlkm5HzBq5hGNjvzdiHu-sZM6ZUXx0hmkkeyfw-L6pWcz0KD58k~X3h9MreLUswmNhoSsrTnL-5njmboYUyScmenF-pl17lZee-4pJJG-tNcEktVCwo9QGC1rDrv1sEbn9mqyb9UNXa3bqp74tk8b~6palnPPQM9rO2-Uj~fws6lbaEFii5mT7gzua87hSLc6NpNm0C8mUL6oDg9q5cmNcanGOFlpPMRX8-tEgvuXML7m6TBuZH8etnhanZaFa0-fogKnr~s4y4jpkAKVpMPMa56m7zNrdbW4cA__&Key-Pair-Id=APKAICJI44KY7SDYRFQQ" | base64 -d
결과값
{"Statement":[{"Resource":"http://d1e5lqevy0hhfg.cloudfront.net/test.mp4","Condition":{"IpAddress":{"AWS:SourceIp":"xxx.xxx.xxx.xxx/24"},"DateLessThan":{"AWS:EpochTime":1488263595}}}]}
인증 실패 시 디버깅에 필요할 수도 있기 때문에 디코딩 관련 내용을 첨부했음.
Cookie의 경우 Value가 Cookie 값으로 전달되고, expire 시간이 지나면 AcceessDenied 메시지가 출력되며 403 코드를 리턴.
5. 사용 방법
1. Document를 따라 Policy json 파일을 생성(공란 제거)
2. AWS Web Console에서 다운받은 private key를 이용하여 Policy json을 RSA 또는 SHA1 해쉬 생성
3. 해쉬 값 Base64 인코딩
4. URL에 영향을 끼치는 +, =, / 을 -, _, ~로 값 치환
5. URL/Cookie에 Policy, Signature, Key-Pair-Id 값 등록
AWS에서는 Java, PHP 등 SDK를 제공하고 있습니다. SDK를 사용할 경우 library(CloudFrontClient)에서 3, 4, 5에 대한 작업을 한다. Key File 권한 확인해야 한다.
Signed URL Sample(PHP 5.5+)
<?php
function createSignedURL($streamHostUrl, $resourceKey, $timeout){
$keyPairId = "APKAJPWDZQR7UI2ARAYQ"; // Key Pair
$expires = time() + $timeout; // Expire Time
$url = $streamHostUrl . '/' . $resourceKey; // Service URL
$ip=$_SERVER["REMOTE_ADDR"] . "\/24"; // IP
$json = '{"Statement":[{"Resource":"'.$url.'","Condition":{"IpAddress":{"AWS:SourceIp":"'.$ip.'"},"DateLessThan":{"AWS:EpochTime":'.$expires.'}}}]}';
$fp=fopen("/home/ec2-user/pk-APKAJPWDZQR7UI2ARAYQ.pem", "r");
$priv_key=fread($fp, 8192);
fclose($fp);
$key = openssl_get_privatekey($priv_key);
if(!$key){
echo "<p>Failed to load private key!</p>";
return;
}
if(!openssl_sign($json, $signed_policy, $key, OPENSSL_ALGO_SHA1)){
echo '<p>Failed to sign policy: '.opeenssl_error_string().'</p>';
return;
}
$base64_signed_policy = base64_encode($signed_policy);
$policy = strtr(base64_encode($json), '+=/', '-_~'); //Custom Policy
$signature = str_replace(array('+','=','/'), array('-','_','~'), $base64_signed_policy);
//Construct the URL
//$signedUrl = $url.'?Expires='.$expires.'&Signature='.$signature.'&Key-Pair-Id='.$keyPairId; //Manual Policy
$signedUrl = $url.'?Policy='.$policy.'&Signature='.$signature.'&Key-Pair-Id='.$keyPairId; //Custom Policy
return $signedUrl;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Signed URL Test</title>
</head>
<body>
<div>
<?php $signedUrl = createSignedUrl('http://d3cqtg62iu3s25.cloudfront.net', 'test.mp4', 30);?>
<?php echo $signedUrl ?>
</div>
<video controls autoplay>
<source src="<?php echo $signedUrl ?>" type="video/mp4">
</video>
</body>
</html>
Signed URL Sample(PHP 5.5+ AWS SDK 사용)
<?php
require 'vendor/autoload.php'; // include autoloader of composer.
use Aws\CloudFront\CloudFrontClient;
function createSignedUrl($streamHostUrl, $resourceKey, $timeout){
$cloudFront = new Aws\CloudFront\CloudFrontClient([
'region' => 'ap-northeast-2',
'version' => '2014-11-06'
]);
$url = $streamHostUrl . '/' . $resourceKey;
$expires = time() + $timeout;
$ip = $_SERVER['REMOTE_ADDR'] . "\/24";
$json = '{"Statement":[{"Resource":"'.$url.'","Condition":{"IpAddress":{"AWS:SourceIp":"'.$ip.'"},"DateLessThan":{"AWS:EpochTime":'.$expires.'}}}]}';
$signedUrlCustomPolicy = $cloudFront->getSignedUrl([
'url' => $streamHostUrl . '/' . $resourceKey,
// 'expires' => $expires, //Manual Policy
'policy' => $json, //Custom Policy
'private_key' => '/home/ec2-user/pk-APKAJPWDZQR7UI2ARAYQ.pem',
'key_pair_id' => 'APKAJPWDZQR7UI2ARAYQ'
]);
return $signedUrlCustomPolicy;
}
error_reporting(E_ALL);
ini_set("display_errors", 1);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Signed URL Test</title>
</head>
<body>
<div>
<?php $signedUrl = createSignedUrl('http://d3cqtg62iu3s25.cloudfront.net', 'test.mp4', 30);?>
<?php echo $signedUrl ?>
</div>
<video controls autoplay>
<source src="<?php echo $signedUrl ?>" type="video/mp4">
</video>
</body>
</html>
Policy에 사용자가 만든 json 파일을 expires 대신 추가하면 사용자 지정 정책이 된다. json 파일은 공백이 없어야 하며, rsa로 암호화 한 뒤에 base64로 인코딩한 뒤에 URL에 영향을 끼치는 몇몇 문자를 변경해야 한다. 이런 잡다한 작업은 SDK를 사용하면 CloudFrontClient.php라는 라이브러리에서 알아서 해준다.
Signed Cookie Sample(PHP 5.5+)
<?php
function createSignedCookie($streamHostUrl, $resourceKey, $timeout){
$keyPairId = "APKAJPWDZQR7UI2ARAYQ"; // Key Pair
$expires = time() + $timeout; // Expire Time
$url = $streamHostUrl . '/' . $resourceKey; // Service URL
$ip=$_SERVER["REMOTE_ADDR"] . "\/24"; // IP
$json = '{"Statement":[{"Resource":"'.$url.'","Condition":{"IpAddress":{"AWS:SourceIp":"'.$ip.'"},"DateLessThan":{"AWS:EpochTime":'.$expires.'}}}]}';
$fp=fopen("/home/ec2-user/pk-APKAJPWDZQR7UI2ARAYQ.pem", "r");
$priv_key=fread($fp, 8192);
fclose($fp);
$key = openssl_get_privatekey($priv_key);
if(!$key){
echo "<p>Failed to load private key!</p>";
return;
}
if(!openssl_sign($json, $signed_policy, $key, OPENSSL_ALGO_SHA1)){
echo '<p>Failed to sign policy: '.opeenssl_error_string().'</p>';
return;
}
$base64_signed_policy = base64_encode($signed_policy);
$policy = strtr(base64_encode($json), '+=/', '-_~'); //Custom Policy
$signature = str_replace(array('+','=','/'), array('-','_','~'), $base64_signed_policy);
//Construct the URL
//$signedUrl = $url.'?Expires='.$expires.'&Signature='.$signature.'&Key-Pair-Id='.$keyPairId; //Manual Policy
$signedCookie = array(
"CloudFront-Key-Pair-Id" => $keyPairId, "CloudFront-Policy" => $policy, "CloudFront-Signature" => $signature );
return $signeCookie;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Signed cookie Test</title>
</head>
<body>
<?php
$signedCookieCustomPolicy = createSignedCookie('http://cf.leedoing.com', 'vod/*', 300);
foreach ($signedCookieCustomPolicy as $name => $value) {
setcookie($name, $value, 0, "", "leedoing.com", false, true);
}
print_r($signedCookieCustomPolicy);
?>
</body>
</html>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="video"></video>
<script>
if(Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls();
hls.loadSource('http://cf.leedoing.com/vod/mp4:sample.mp4/playlist.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
}
</script>
Signed Cookie Sample(PHP 5.5+, PHP v3 SDK)
<?php
require 'vendor/autoload.php'; // include autoloader of composer.
use Aws\CloudFront\CloudFrontClient;
function createSignedCookie($streamHostUrl, $resourceKey, $timeout){
$cloudFront = new Aws\CloudFront\CloudFrontClient([
'region' => 'ap-northeast-2',
'version' => '2014-11-06'
]);
$url = $streamHostUrl . "/" . $resourceKey;
$ip = $_SERVER['REMOTE_ADDR'] . "\/24";
$expires = time() + $timeout;
$json = '{"Statement":[{"Resource":"'.$url.'","Condition":{"IpAddress":{"AWS:SourceIp":"0.0.0.0\/0"},"DateLessThan":{"AWS:EpochTime":'.$expires.'}}}]}';
$signedCookieCustomPolicy = $cloudFront->getSignedCookie([
'url' => $url, //Manual Policy
// 'expires' => $expires, //Manual Policy
'policy' => $json, //Custom Policy
'private_key' => '/home/ec2-user/pk-APKAJPWDZQR7UI2ARAYQ.pem',
'key_pair_id' => 'APKAJPWDZQR7UI2ARAYQ'
]);
return $signedCookieCustomPolicy;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Signed cookie Test</title>
</head>
<body>
<?php
$signedCookieCustomPolicy = createSignedCookie('http://cf.leedoing.com', 'vod/*', 300);
foreach ($signedCookieCustomPolicy as $name => $value) {
setcookie($name, $value, 0, "", "leedoing.com", false, true);
}
print_r($signedCookieCustomPolicy);
?>
</body>
</html>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="video"></video>
<script>
if(Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls();
hls.loadSource('http://cf.leedoing.com/vod/mp4:sample.mp4/playlist.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
}
</script>
Signed Cookie 또한 방식은 URL과 거의 동일합니다. 그러나 Path 별로 Singed Cookie를 사용할 수 있습니다.
만약 Signed URL/Cookie를 통해 CloudFront 접근 시에 MalformedPolicy 라는 값이 출력되면 Policy에 문제가 있으니 수정해주시길 바랍니다. 공백은 없는지, RSA 암호화는 되어 있는지, base64 인코딩은 되어 있는지 확인해봅니다.
추가로 node.js SDK에서도 언제부턴가 CloudFront Signer를 지원한다.
또한 aws-cloudfront-sign이라는 라이브러리가 존재한다. 사용자 정책 기반이며 단 4줄로 구현 가능하다.
e.g
var cf = require('aws-cloudfront-sign')
var options = {keypairId: 'APKAIASXXXXJAVPWH3GA', privateKeyPath: './private.pem'}
var signedUrl = cf.getSignedUrl('http://d1rl3jmwx11wpl.cloudfront.net/my.mp4', options);
console.log('Signed URL: ' + signedUrl);
Signed Cookie 테스트의 경우 ffmpeg / ffplay를 이용하면 조금 더 편하다.
'Storage&CDN > CloudFront' 카테고리의 다른 글
AWS CloudFront Apache 원본 연동(mod_security2) (0) | 2018.02.11 |
---|---|
AWS CloudFront RTMP 재생 URL (0) | 2017.04.11 |
AWS CloudFront 및 기타 CDN 성능 측정 솔루션(Keynote) (0) | 2016.09.21 |
Amazon CloudFront SSL 인증서 설정하기 (0) | 2016.08.17 |
Amazon CloudFront 동적 컨텐츠 캐싱 및 주요 기능 (0) | 2016.01.18 |