یک پیوند امضاشده واقعاً چه وعدهای میدهد
سه چیز، و ارزش دارد دقیق باشیم چون افراد انتظار یک چهارمی را دارند.
ثابت میکند پیوند از طرف شما آمده. signature یک hash از مسیر، expiry و یک secret است که فقط سرور شما میداند. هیچکس بدون secret نمیتواند یک پیوند کارآمد جعل کند.
منقضی میشود. expiry بخشی از چیزی است که امضا شده، پس بدون شکستن signature قابلویرایش نیست.
پیش از دستزدن به origin شما بررسی میشود. در یک edge CDN، یک درخواست امضانشده در همان edge رد میشود؛ سرور شما هرگز آن را نمیبیند.
آنچه وعده نمیدهد: اینکه کسی که پیوند را باز کرده فایل را ذخیره و برای دوستی ایمیل نمیکند. یک پیوند امضاشده دسترسی به پیوند را کنترل میکند، نه بایتها را. اگر بعد از دانلود به کنترل نیاز دارید، دنبال DRM میگردید، که محصولی متفاوت و بسیار سنگینتر است.
دو خانواده: signatureهای edge و presigned URLها
path signatureها در edge — چیزی که یک CDN به شما میدهد. اپلیکیشن شما یک signature برای یک مسیر و یک expiry محاسبه میکند؛ edge هنگام رسیدن آن را verify میکند. فایل میتواند هر جایی که CDN بتواند برسد زندگی کند، و بررسی نزدیک بازدیدکننده اتفاق میافتد.
presigned URLها از object storage — چیزی که S3 و ذخیرهسازی سازگار با S3 به شما میدهند. خودِ سرویس ذخیرهسازی یک URL موقت صادر میکند، امضاشده با access key شما، معمولاً برای چند دقیقه معتبر. signature در query string بهشکل X-Amz-Signature و مشابه آن سفر میکند.
آنها رقیب هم نیستند. یک چیدمان معمول و منطقی، object storage پشت یک CDN است، با signature edge که URL عمومی را محافظت میکند و bucket خصوصی نگهداشتهشده تا هیچکس نتواند از edge دور بزند. کاری که نباید بکنید این است که یک presigned storage URL را مستقیم در معرض عموم قرار دهید و فرض کنید CDN آن را محافظت میکند — اگر پیوند به bucket اشاره میکند، edge اصلاً در مسیر نیست.
چگونه در cdn.com.tr کار میکند
این یک تنظیم روی یک قانون تحویل (delivery rule) است، پس میتوانید /downloads را محافظت کنید درحالیکه بقیهٔ سایت عمومی میماند. برای آن قانون پیوندهای منقضیشونده را روشن کنید، یک secret تنظیم کنید، و در اپلیکیشن خودتان پیوند تولید کنید.
signature شکل base64url از MD5 خام سه چیز است که به هم پیوستهاند: expiry، مسیر، و secret با یک فاصلهٔ منفرد پیش از آن.
``` $uri = "/downloads/report.pdf"; $expires = time() + 600; // ten minutes $secret = "your-long-random-secret";
$token = rtrim(strtr(base64_encode( md5($expires . $uri . " " . $secret, true) ), "+/", "-_"), "=");
$url = "https://cdn.example.com{$uri}?md5={$token}&expires={$expires}"; ```
سه نتیجه، و عمداً متمایزند تا لاگهای شما بتوانند آنها را از هم تشخیص دهند:
| درخواست | پاسخ | |---|---| | signature معتبر، منقضینشده | فایل، cacheشده بهشکل عادی | | بدون signature، یا یک signature اشتباه | 403 | | signature درست، expiry گذشته | 410 |
آن 410 بیشتر از آنچه به نظر میرسد اهمیت دارد. وقتی یک مشتری میگوید «پیوند شما خراب است»، فقط کد وضعیت به شما میگوید آیا یک پیوند بد برایش فرستاده شده یا فقط بیشازحد صبر کرده — بدون اینکه چیزی از او بپرسید.
تلهٔ cacheای که CDN را از شما میگیرد
این بخشی است که در اکثر مستندات غایب است، و همان بخشی است که آسیب میزند.
هر پیوند امضاشده منحصربهفرد است: یک md5 متفاوت و یک expires متفاوت برای هر بازدیدکننده و هر صدور. اگر cache key شما query string را شامل شود — که پیشفرض معمول است، و مالِ ماست مگر آن را تغییر دهید — آنوقت هر درخواست امضاشده یک ورودی cache جداست. نسبت hit برای فایلهای محافظتشده به صفر میرسد، هر دانلود از origin شما گرفته میشود، و شما بابت CDNای پول دادهاید که دارد مثل یک proxy عمل میکند.
راهحل یک تنظیم است. روی قانون تحویل، حالت query-string را طوری تنظیم کنید که پارامترهای signature وارد cache key نشوند:
- Ignore list — md5 و expires را نادیده بگیرید، هر چیز معنادار دیگری را نگه دارید. این انتخاب درست است وقتی همان فایل با پارامترهای واقعی هم گرفته میشود.
- Ignore all — سادهترین گزینه وقتی مسیرهای محافظتشده اصلاً پارامتر معناداری نمیگیرند، که معمولاً برای دانلودها درست است.
بعداً آن را بررسی کنید نه اینکه فرض کنید: همان فایل را دوبار با دو signature معتبر متفاوت بخواهید و به هدر وضعیت cache نگاه کنید. دومی باید یک hit باشد. اگر هر دو miss بودند، key هنوز signature را در خود دارد.
انتخاب یک expiry
بهاندازهٔ کافی کوتاه که یک پیوند leakشده بیارزش باشد؛ بهاندازهٔ کافی طولانی که دانلود روی یک کانکشن بد تمام شود.
اسناد و تصاویر: پنج تا پانزده دقیقه. بلافاصله بعد از کلیک گرفته میشوند.
ویدیوی بزرگ و آرشیوها: یک تا شش ساعت. یک فایل دوگیگابایتی روی یک کانکشن موبایل کند بیشتر از آنچه افراد انتظار دارند طول میکشد، و یک expiry که وسط دانلود فعال شود یک تیکت پشتیبانی تولید میکند که شبیه خرابی فایل به نظر میرسد.
manifestهای streaming دقت میخواهند. با HLS، player اول manifest و بعد بسیاری segment را در طول مدت پخش میگیرد. اگر هر segment را با یک expiry کوتاه امضا کنید، پخش وسط یک ویدیوی بلند میشکند. با یک expiry که کل session را پوشش میدهد امضا کنید، یا manifest را امضا کنید و segmentها را با یک کنترل متفاوت محافظتشده رها کنید.
یک چیزی که نمیتوانید انجام دهید revokeکردن یک پیوند صادرشدهٔ منفرد است. تا expiryاش معتبر است، تمام. اگر به revoke فوری نیاز دارید، secret را بچرخانید (rotate) — که هر پیوندی را که صادر کردهاید بیاعتبار میکند.
اشتباهاتی که secret را leak میکنند
امضاکردن در مرورگر. اگر secret در JavaScript باشد، عمومی است. امضاکردن همیشه متعلق به سرور است. این بدیهی به نظر میرسد و رایجترین راه واحدی است که secretها فرار میکنند.
پختهکردن پیوندها در زمان build. پیوندی که در طول یک build استاتیک تولید میشود یک expiry حمل میکند که در زمان build تنظیم شده، و درحالیکه صفحه هنوز زنده است منقضی میشود. در زمان درخواست تولید کنید، یا از طریق یک endpoint کوچک که ریدایرکت میکند.
Clock skew. expiry با ساعت edge مقایسه میشود. اگر سرور اپلیکیشن شما چند دقیقه عقب باشد، پیوندها منقضی متولد میشوند. NTP را در حال اجرا نگه دارید؛ وقتی یک پیوند بلافاصله بعد از صدور شکست میخورد، پیش از کد به ساعت نگاه کنید.
گذاشتن secret در یک repository. آن را مثل یک credential در نظر بگیرید: environment variable، نه source control. وقتی کسی تیم را ترک میکند، آن را بچرخانید.
فراموشکردن اینکه rotation سراسری است. تغییر secret هر پیوند در جریانی را یکباره بیاعتبار میکند، از جمله آنهایی که در ایمیلهای پنج دقیقه پیش فرستاده شدهاند. این دقیقاً همان چیزی است که در طول یک حادثه میخواهید و دقیقاً همان چیزی است که یک بعدازظهر جمعه بهطور اتفاقی نمیخواهید.
پرسشهای پرتکرار
فرق یک پیوند امضاشده با یک presigned URL چیست؟
بیشتر اینکه بررسی کجا اتفاق میافتد. «presigned URL» اصطلاح S3 برای یک پیوند موقت است که توسط سرویس ذخیرهسازی صادر و توسط خودش verify میشود. یک پیوند امضاشده در یک edge CDN در همان edge، نزدیک بازدیدکننده، verify میشود و برای هر originای کار میکند نه فقط یک bucket. مفهوم یکسان است: یک signature بهعلاوهٔ یک expiry در query string.
آیا کسی میتواند یک پیوند امضاشده را بهاشتراک بگذارد؟
بله، تا وقتی منقضی شود. یک signature ثابت میکند پیوند توسط شما صادر شده؛ چیزی دربارهٔ اینکه چهکسی آن را در دست دارد نمیگوید. expiryهای کوتاه آسیب را محدود میکنند. اگر نیاز دارید پیوند به یک نفر مقید باشد، چیزی شناساییکننده در مسیر امضاشده بگنجانید و سمت خودتان بررسی کنید، و بپذیرید که یک بهاشتراکگذارندهٔ مصمم همچنان میتواند فایل دانلودشده را forward کند.
آیا امضاکردن به cache آسیب میزند؟
فقط اگر بگذارید signature وارد cache key شود، و آنوقت بهشدت آسیب میزند — هر پیوند منحصربهفرد ورودی cache خودش میشود و هر دانلود به origin شما میرود. حالت query-string را طوری پیکربندی کنید که پارامترهای signature را نادیده بگیرد و فایل بهشکل عادی cache میشود، مشترک بین هر کسی که یک پیوند معتبر دارد.
آیا میتوانم یک پیوند را revoke کنم؟
نه بهطور مجزا. یک signature تا expiryاش معتبر است چون verification بدونوضعیت (stateless) است — هیچ لیستی برای حذفکردن از آن نیست. کنترلهای موجود یک expiry کوتاه و چرخاندن secret است، که همهچیز را یکجا بیاعتبار میکند.
آیا MD5 برای این کار شکسته نیست؟
MD5 از نظر مقاومت در برابر collision شکسته است، که وقتی یک مهاجم میتواند هر دو پیام را انتخاب کند اهمیت دارد. اینجا مهاجم باید یک hash از رشتهای جعل کند که حاوی secretای است که ندارد، که یک مشکل preimage است نه collision. ریسک عملی leakشدن یا قابلحدسبودن secret است — از یک secret تصادفی طولانی استفاده کنید، آن را سمت سرور نگه دارید، و بچرخانیدش.
آیا باید از این برای ویدیوی HLS استفاده کنم؟
کار میکند، با این نکتهٔ احتیاطی دربارهٔ طول session: expiry باید کل پخش را پوشش دهد، نه فقط درخواست manifest، وگرنه پخش وسط راه متوقف میشود. برای محتوای بلند، یک expiry سخاوتمندانه را با rate limiting جفت کنید، نه یک expiry بسیار کوتاه که player را میشکند.