やった!3Dデータを作ったぞ! 3Dプリントもできるし、モデルデータ販売サイトにも登録した! よーし、サイトに公開してみんなに周知しよう! ………これ、配置しただけだとスクレイピングでデータ窃取できちゃうな…。 サイバー空間にアップしたいけど上げれない、そんなジレンマがあるかと思います。 今回は本サイトでも採用している回避方法を紹介します。
まずやりたいこととその問題を洗い出していきましょう。 <やりたいこと> ・3Dモデルをサイト上に公開したい。 ・回転させたり拡大縮小させたりしてぐりぐり操作したい。 ・パスは隠してDLできないようにしたい。 ・サイトへの導入はシンプルに、使いまわせるようにしたい。 <問題> ・3Dビューアを作るのはコストがかさむ。 → model-viewerを使う。 ・Wordpressのメディアライブラリでは3Dモデルを扱えない。 ・クライアント側(model-viewer)にモデルのパスを渡す形では、DLできてしまうしディレクトリトラバーサルされる危険性もある。 クライアント側(JavascriptとCSS)だけではちょっと解決が難しいかもしれません。 サーバ側のことはあんまりよくわからないので、こういう時はGPTちゃんに聞くことにします。
GPTちゃん、頼りになるわぁ。 ユーザ側に表示されている以上完全に防御することは不可能。 まぁそうだよね。 とはいえ、し難くすることはできそうな感じです。 アクセス制限やログイン制にするのは本末転倒なので、glTFストリーミングする方向で考えていきましょう。 ただし、分割処理や暗号複合、アクセス制限などはコスト高めなので、とりあえず”サーバから生データでモデルをもらう”という形にしておきましょう。 さて、全体の構成を考えていきましょう。
構成はいたってシンプル。 model-viewerはWordpressのカスタムURLで記述、データ問い合わせとViewerへのセットを担当するJS、サーバ側のデータ処理を担当するPHPで構成されています。 1.model-viewerが表示されるのをトリガーに、PHPにモデル名をFetch。 2.PHPでモデル名を検索して、見つかればモデルの生データをSend。 3.データが返ってきたら生データにURLを付与してmodel-viewerのsrcに流し込む。 というシーケンスです。 では カスタムHTML ⇒ JS ⇒ PHP の流れで見ていきましょう。
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
<model-viewer class="mviewer" name="modelname" loading="lazy" type="model/gltf-binary" exposure="0.6"></model-viewer>
document.addEventListener("DOMContentLoaded", () => {
var mvlist = document.querySelectorAll(".mviewer");
if(mvlist){
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if(entry.isIntersecting){
loadmodel(entry.target);
observer.unobserve(entry.target);
}
})
})
mvlist.forEach(mv => {observer.observe(mv);});
}
});
function loadmodel(target){
const fileName = target.getAttribute("name") + ".glb";
const url = `/wp/response.php`;
fetch(url, {
method: "POST",
headers: { 'Content-Type': 'model/gltf-binary' },
body : fileName
})
.then(res => {
if (!res.ok) throw new Error("取得失敗" + url);
return res.blob();
})
.then(blob => {
const objectURL = URL.createObjectURL(blob);
target.src = objectURL;
})
.catch(err => console.error("モデル読み込みエラー: ", err + url));
}
<?php
if (class_exists( 'loadmodel' ) ) {
error_log("double exist ");
return;
}
$filename = file_get_contents('php://input');
if (!preg_match('/^[a-zA-Z0-9_\-]+\.glb$/', $filename)) {
error_log("loadmodel not glb : " . $filename);
http_response_code(400);
exit("Invalid file name");
}
$baseDir = __DIR__ . '/wp-content/uploads/model/';
$filePath = $baseDir . $filename;
if (!$filePath || strpos($filePath, realpath($baseDir)) !== 0) {
error_log("loadmodel access denied : " . $filePath);
http_response_code(403);
exit("Access denied");
}
if (!file_exists($filePath)) {
error_log("loadmodel not found : " . $filePath);
http_response_code(404);
exit("File not found");
}
header('Content-Type: model/gltf-binary');
header('Content-Disposition: inline; filename="'.$filename.'"');
header('Content-Length:' . filesize($filePath));
readfile($filePath);
exit;
?>
JSでは、intersectingObserverでmodelviewerの表示を検知しFetchを実行します。 Fetchでは指定したPHP(response.php)にファイル名を投げて処理を依頼。 レスポンスが返ってきたら中身をチェックして、問題なければ中身にURLを付与してModelViewerのSrcに渡します。 なお、表示されるたびに実行されても無駄なので一回実行したらObserve対象から外しています。 PHPはかなりシンプルで、Fetchでもらったファイル拡張子のチェック・パスのチェック・レスポンスヘッダの記述・生データの取り出しという流れです。 注意点として、PHPはhttpから始まるURLは処理できません。 パスを渡すときはローカルパスで指定するようにしましょう。 PHPは全く触ったことがなかったので、ここでかなり躓いてしまいました…。 なぜかPHPの純正ログも出ないしWordpressのログも使えないしよー……チクショー…。 重要なのはcontent-typeにmodel/gltf-binaryを指定すること。 Fetchで指定するかレスポンスで指定するか、どちらかでいいと思います。たぶん。 今回は念のため両方とも書いてます。
いい感じですね! DevToolのインスペクターにはURLも書かれていませんし、PHPからの応答も生データなのでデータの窃取もしにくくなっています。 場合によってはサーバがglb,gltfに対応してない場合があります。 その場合は.htaccessにてMIMEタイプの追加をしてみるといいかもしれません。 <IfModule mime_module> AddType model/gltf-binary .glb </IfModule> この仕組みは、Content-typeを変えれば3Dデータだけでなく画像や動画にも応用できそうですね。 良ければご活用ください。