コロナウイルス対応のため、学習コンテンツの無料開放にひと肌脱いだ話。


0.まえおき

新型コロナウイルスによる、学校の休校を受け、
教育業界において、コンテンツの無料開放などが流行っています。

そんなさなか、とある老舗の参考書出版会社さまから、
コンテンツ無料解放をしたいと相談を受けたのが金曜日の夜。。。

すばらしい社会貢献!
ひと肌脱ぐことになり、私の休日(ほぼ徹夜)は消え去りました。
(えぇ、もちろん私も無償奉仕です。)

1.要件/状況

・PDFの書籍データがある
・誰にでも見せたいけど、ダウンロードとか印刷をされてしまっては困る。
 (過度には誰にも見せたくない、、、)
・サーバとか何もない

という、本当にゼロからのスタートです;;






2.実現方法概要

1.PDF(URL)の保護
 PDF自体のDRMとかもあるのかもしれないですが、Adobeが絡むはずなので、(あまり詳しくないです)
 AWSの署名付きURLで期限付きのURLを発行して、URLの流出やクローラに対応。

2.PDFのDLや印刷を禁止する
 ブラウザでのPDF閲覧ではなく、独自Viewer(PDF.js)を採用し、DF閲覧時の機能を制限する。

3.時間もないためにサーバレスで実装
 静的コンテンツであれば、関連するシステム上に配置可能とのことで、
 それ以外については、AWSを多用しまくります。

4.致し方ないこと
 ・それなりにITの知識がある人にはどうしてもPDFデータはDLされちゃう。
 ・画面キャプチャなど、OSによる機能は防げない。






3.という事で、全体のシーケンス

もっと、いい案がないのか、、、と思いつつも時間がないので、即実装です。
 

①AWS API Gateway
 ・GETリクエストで、閲覧したい書籍のパラメータを受ける。
 ・APIについては、IP制限が使えないため(IP固定なのか不明・・・)、アクセス元ドメインでのリファラー制限を実施。
   (リファラー詐称をされない限り、APIは実行不可)

③AWS Lamda
 ・受け取った書籍情報について、S3上のPDFに対し署名付きURLを発行する。
 ・そのままURLを返却してしまうと通常のブラウザで見えてしまうので、
  署名付きURLを付与したViewerのURLへリダイレクトさせる。

③PDF.js
 ・パラメータにある署名付きURLのデータをPDF.jsが読み込む。
  (この際、PDF.jsのパラメータを見て、ヨシヨシ!という感の鋭い人には、PDFそのもののアクセスができちゃう。
   ただ、URLがすごく長いのでぱっと見わからないかも。)

④公開領域フォルダ
 ・DLや印刷機能を禁止したカスタマイズ版のPDF.jsを配置します。

⑤PDFの書籍データ
 ・署名付きURLでしかアクセスできないようにします。
 ・Viewerからアクセスされるので特定ドメインのCORS設定も行います。






4.各々の詳細

①②AWSのLambdaコードです。(node.js v12.Xです)

ポイントは、Lambda プロキシ統合利用でのCORS設定と、
302リダイレクトのあたりでしょうか。

APIGatewayとLambdaについては、前回の記事も参考ください。
AWS APIGateway + Lambda プロキシ統合 の利用でCORS設定にはまった話

署名付きURLの発行と302リダイレクト
const https = require('https');

const AWS_ACCESSKEY_ID =  process.env.AWS_ACCESSKEY_ID;
const AWS_SECRET_ACCESSKEY =  process.env.AWS_SECRET_ACCESSKEY;
const PDF_BASE_URL = process.env.PDF_BASE_URL;
const PDF_BUCKET = process.env.PDF_BUCKET;
const PDF_EXPIRES = parseInt(process.env.PDF_EXPIRES,10);

let AWS = require('aws-sdk');
AWS.config.update({accessKeyId: AWS_ACCESSKEY_ID, secretAccessKey: AWS_SECRET_ACCESSKEY,region:"ap-northeast-1"});
let s3 = new AWS.S3({signatureVersion:'v4'});


exports.handler = async function (event, context, callback) {

    const done = (err, res) => callback(null, {
        statusCode: err ? '400' : '302',
        body: res,
        headers: {
            "Access-Control-Allow-Origin": "*", //Lambdaプロキシ統合の場合は自前でCORS設定。
            "Location": res //302リダイレクトのために独自設定
        }
    });

    switch (event.httpMethod) {
        case 'GET':

            const book = event['queryStringParameters']['book'];
            const page = event['queryStringParameters']['page'];

            let presignedURL = await createPresignedURL(PDF_BUCKET,book+"/"+page+".pdf");
            let return_url = PDF_BASE_URL+"?file="+encodeURIComponent(presignedURL);
            console.log('createPresignedURL :' + return_url);  

            done(null,return_url);

            break;
        default:
            done(new Error(`Unsupported method "${event.httpMethod}"`));
    }

};


/**
 * 対象のURLに対して署名を行い、公開用のURLを発行します。
 */
async function createPresignedURL(inBucket,pdffilename){

    const s3params = {
        Bucket: inBucket,
        Key: pdffilename,
        Expires: PDF_EXPIRES
    };
    console.log('createPresignedURL:', JSON.stringify(s3params));

    try {
        let signed_url = await s3.getSignedUrl('getObject', s3params);
        return signed_url;
    } catch (err) {
        console.log("Err:"+err);
        return err;
    }

}

また、API GateWay側にはリダイレクト設定が必要です。

あとは、APIGateWayのリソースポリシー設定を行います。
APIGateway全般なんですが、設定などを変えた後、「APIのデプロイ」を忘れがちです。。。

リソースポリシー
{
    "Version": "2012-10-17",
    "Id": "policy example",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:Hogehoge/*",
            "Condition": {
                "StringLike": {
                    "aws:Referer": [
                        "https://foobar/*"
                    ]
                }
            }
        }
    ]
}




③PDF.js(pdfjs-2.2.228)の修正

・PDF.jsについては、DLボタンの禁止
・印刷の禁止

・縦スクロールを見やすい横スクロールに
・HOSTED_VIEWER_ORIGINSの設定
などを実施しています。

viewer.css
 /* ツールバーのボタン類を削除 */
#sidebarToggle,
#openFile, 
#viewBookmark,
#print,
#download,
#secondaryOpenFile, 
#secondaryViewBookmark,
#secondaryDownload,
#secondaryPrint,
#secondaryToolbarToggle { 
    display: none !important;
}

 /* ブラウザ印刷時のデータ削除 */
@media print{
  body{
    display: none;
  }
}
viewer.js(13900行目あたり)
function getDefaultPreferences() {
  if (!defaultPreferences) {
    defaultPreferences = Promise.resolve({
      "cursorToolOnLoad": 0,
      "defaultZoomValue": "",
      "disablePageLabels": false,
      "enablePrintAutoRotate": false,
      "enableWebGL": false,
      "eventBusDispatchToDOM": false,
      "externalLinkTarget": 0,
      "historyUpdateUrl": false,
      "pdfBugEnabled": false,
      "renderer": "canvas",
      "renderInteractiveForms": false,
      "sidebarViewOnLoad": -1,
      "scrollModeOnLoad": 1, //ここのデフォルトを-1⇒1に変更
      "spreadModeOnLoad": -1,
      "textLayerMode": 1,
      "useOnlyCssZoom": false,
      "viewOnLoad": 0,
      "disableAutoFetch": false,
      "disableFontFace": false,
      "disableRange": false,
      "disableStream": false
    });
  }




④公開領域バケットの設定

(割愛)
公開領域といえども、無駄なアクセスを避けるため、
気持ち程度にリファラー設定。

バケットポリシー
{
    "Version": "2012-10-17",
    "Id": "policyexample",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::hogehoge/*",
            "Condition": {
                "StringLike": {
                    "aws:Referer": [
                        "https://foo.bar/*"
                    ]
                }
            }
        }
    ]
}




⑤非公開領域バケットの設定

(割愛)
Viewerから呼び出されるために、CORS設定が必要になります。

CORSの設定
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>https://hogehohogehoge</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <ExposeHeader>ETag</ExposeHeader>
    <ExposeHeader>Accept-Ranges</ExposeHeader>
    <ExposeHeader>Content-Encoding</ExposeHeader>
    <ExposeHeader>Content-Length</ExposeHeader>
    <ExposeHeader>Content-Range</ExposeHeader>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>






5.あとがき

サービスサイトから、GETパラメータを渡すことで、無事にPDFの表示ができています。
不用意にコピーされたくない、PDFをWeb上で公開する方法の1案ではないでしょうか。

作成しましたサイトですが、
公開可能でしたら、後日ご紹介したいと思います。
(フロント側については、もう一人のエンジニアの方と実装&ブラッシュアップ中です)

なんだかんだ、時間を要したのは、PDFの加工(原本はGB単位)だったりしました。。。
(PDFを命名ルールを付けながらリネーム、分割、圧縮、結合、ディレクトリへの配置。)