본문 바로가기

Storage&CDN/CloudFront

AWS CloudFront SignedURL/Cookie 사용

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 사용자 정책

 

서명된 URL 사용 - Amazon CloudFront

서명된 URL 사용 서명된 URL에는 만료 날짜 및 시간 같은 추가 정보가 포함되므로 콘텐츠에 대한 액세스를 보다 세부적으로 제어할 수 있습니다. 이러한 추가 정보는 미리 준비된(canned) 정책 또는 사용자 지정 정책에 따라 정책 설명에 나타납니다. 미리 준비된(canned) 정책과 사용자 지정 정책 간의 차이점은 이어지는 두 단원에 설명되어 있습니다. 참고 같은 배포에 대해 미리 준비된(canned) 정책과 사용자 지정 정책으로 각각 서명된 URL을

docs.aws.amazon.com

 

둘의 차이는 아래 그림과 같다. 그냥 사용자 지정 정책을 사용하자. 

사용자 정책을 만들면 인증 시간, Client IP 제어가 가능하다. Policy 변수만 추가하면 사용자 지정 정책이 된다. 사실 AWS에서 굳이 이걸 왜 나눠놨는지 모르겠다. 오히려 헷갈린다. 

Client IP 제어 사용 시 주의점은 IPv6는 지원되지 않는다. 따라서 범용적인 유료서비스의 경우 Client IP 제어는 사용하지 말자.

 

 

4. CloudFront Singed URL vs Cookie

 

서명된 URL과 서명된 쿠키 중 선택 - Amazon CloudFront

서명된 URL과 서명된 쿠키 중 선택 CloudFront 서명된 URL 및 서명된 쿠키는 기본 기능이 같습니다. 바로 콘텐츠에 액세스할 수 있는 대상을 제어하는 기능입니다. CloudFront를 통해 프라이빗 콘텐츠를 제공 중이며 서명된 URL 또는 서명된 쿠키 중 하나를 결정해야 하는 경우, 다음 사항을 고려하십시오. 다음과 같은 경우 서명된 URL을 사용합니다. RTMP 배포를 사용하려는 경우. RTMP 배포에는 서명된 쿠키를 사용할 수 없습니다. 애

docs.aws.amazon.com

 

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. 사용 방법

 

서명된 URL과 서명된 쿠키를 사용하여 프라이빗 콘텐츠 제공 - Amazon CloudFront

서명된 URL과 서명된 쿠키를 사용하여 프라이빗 콘텐츠 제공 인터넷을 통해 콘텐츠를 배포하는 많은 기업에서는 유료 사용자 등 일부 사용자용으로 제작된 각종 문서, 비즈니스 데이터, 미디어 스트림 또는 콘텐츠에 대한 액세스를 제한하고자 합니다. CloudFront를 통해 이러한 프라이빗 콘텐츠를 안전하게 제공하려면 다음과 같이 하십시오. 사용자가 특별한 CloudFront 서명된 URL 또는 서명된 쿠키를 사용하여 프라이빗 콘텐츠에 액세스하도록 합니다. 사

docs.aws.amazon.com

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 권한 확인해야 한다.

 

AWS SDK for PHP

 

docs.aws.amazon.com

 

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를 지원한다. 

 

Class: AWS.CloudFront.Signer — AWS SDK for JavaScript

if a callback is provided, this function will pass the hash as the second parameter (after the error parameter) to the callback function.

docs.aws.amazon.com

 

또한 aws-cloudfront-sign이라는 라이브러리가 존재한다. 사용자 정책 기반이며 단 4줄로 구현 가능하다.

 

aws-cloudfront-sign

Utility module for signing AWS CloudFront URLs

www.npmjs.com

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를 이용하면 조금 더 편하다.

AWS Signed URL/Cookie 테스트

 

 

윈도우 10에서 ffmpeg 사용하기

ffmpeg using on Windows 10 (2018.08.23) ffmpeg은 강력한 encoder/decoder이기 때문에 많은 곳에 사용된다. 하지만 GUI 에서의 사용의 제한으로 인해 command line으로만 사용이 가능하다. (windows key +r ->..

kyoko0825.tistory.com