やった!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データだけでなく画像や動画にも応用できそうですね。
良ければご活用ください。