هر بینندهای به حساب نیاز ندارد
حساب مهمان (که در «دعوت مشتری به یک پروژه» بررسی شد) وقتی درست است که کسی نیاز به تعامل دارد — کامنت بگذارد، تسک اضافه کند، هفتهها پیدرپی وارد شود. برای سرمایهگذاری که میخواهد یک نگاه به پیشرفت بیندازد، یا ذینفعی که به لینکی برای چسباندن در یک ایمیل وضعیت نیاز دارد، اشتباه است. ساختن یک حساب، رمز عبور و جریان دعوت برای یک نگاه پنجدقیقهای اصطکاکی است که هیچکدام طرف نمیخواهد. پاسخ WKFGo یک سازوکار جدا و باریکتر است: یک لینک عمومیِ قابللغو، کاملاً بدون ورود.
توکن کل مدل امنیتی است — پس مثل یکی رفتار میشود
یک ShareLink یک توکن تصادفیِ ۲۴بایتی است، بهصورت هگز کدگذاریشده، با crypto/rand ساخته میشود:
func newToken() string {
b := make([]byte, 24)
rand.Read(b)
return hex.EncodeToString(b)
}
کامنت کد قصد طراحی را مستقیم بیان میکند: «مثل یک توکن بازنشانی رمز عبور رفتارش کنید (غیرقابلحدس، قابللغو، اختیاراً منقضیشونده).» این مدل ذهنی درستی است — لینک اشتراکگذاری «عمومی اما پیداکردنش سخت» نیست، یک اعتبار حامل با همان ویژگیهایی است که از هر اعتبار دیگری میخواهید. ۲۴ بایت تصادفیِ رمزنگاریشده (۱۹۲ بیت) حدسزدن را غیرعملی میکند؛ این ابهام نیست، این خودِ کنترل دسترسی است.
پیدا-نشد در برابر لغوشده — عمداً یک جواب
بازکردن یک توکن سه چیز را چک میکند: آیا وجود دارد، آیا صراحتاً لغو شده، و آیا منقضی شده:
func (l *ShareLink) Live() bool {
if l.RevokedAt != nil {
return false
}
if l.ExpiresAt != nil && l.ExpiresAt.Before(time.Now()) {
return false
}
return true
}
و تابع بازکردن دقیقاً همان خطای عمومیِ «پیدا نشد» را برمیگرداند، چه توکن هرگز وجود نداشته، چه منقضی شده، چه فعالانه لغو شده باشد:
if err := s.db.First(&link, "token = ?", token).Error; err != nil {
return nil, errors.New("not found")
}
if !link.Live() {
return nil, errors.New("not found")
}
این همان اصل پشت برگرداندن یک پیام عمومیِ «اعتبار نامعتبر» توسط نقطهپایانی ورود بهجای تفکیک «رمز عبور غلط» از «چنین کاربری نیست» است — اگر یک لینک لغوشده متفاوت از یک لینک منقضیشده جواب بدهد، یا یک توکن هرگز-صادرنشده، این به کسی اجازه میدهد بفهمد کدام توکنها قبلاً کار میکردند، که همان اطلاعاتی است که نباید نشت کند.
آنچه یک مهمان واقعاً میبیند
بوردی که لینک اشتراکگذاری نشان میدهد پروژهی واقعی نیست — یک تصویر ساختهشده و لختشده برای همین هدف است:
type PublicTask struct {
ID uint `json:"id"`
Title string `json:"title"`
Labels []string `json:"labels,omitempty"`
Progress float64 `json:"progress"`
Priority string `json:"priority,omitempty"`
DueDate *time.Time `json:"dueDate,omitempty"`
Assignees []string `json:"assignees,omitempty"` // display names only
}
بدون شناسهی تسک فراتر از خود تسک، بدون شناسهی کاربر داخلی، بدون ایمیل، بدون نرخ، بدون پیوست، بدون لینک به هیچچیز دیگری در سیستم. مسئولین اسمهای نمایشی هستند، نه حساب. این یک ساختار اختصاصیِ ساختهشده برای همین یک هدف است — نه مدل معمولیِ Task با فیلترشدن فیلدها هنگام سریالایز، که جایی راحت است برای نشتکردن اتفاقیِ فیلدی که یک تغییر اسکیمای آینده اضافه میکند. ساختن شکل عمومی بهعنوان نوع خودش یعنی اضافهکردن یک فیلد حساس به مدل واقعی Task خودکار آن را اینجا فاش نمیکند؛ کسی باید صراحتاً به PublicTask هم اضافهاش کند.
لغو دائمی است، اما رکورد حذف نمیشود
لغو یک لینک RevokedAt را تنظیم میکند بهجای حذف ردیف:
func (s *Service) Revoke(id uint) error {
now := time.Now()
return s.db.Model(&ShareLink{}).Where("id = ?", id).Update("revoked_at", now).Error
}
توکن همان لحظهای که Live() نادرست ارزیابی میشود کار نمیکند — اما ردیف برای مسیر ممیزی میماند: چهکسی ساختش، کِی، چند بار دیده شده، آخرین بار کِی دیده شده. یک لینک لغوشده فراموش نمیشود، فقط مرده است.
اشتباهات رایج
قابلکشفکردن یک لینک «عمومی» با حدسزدن الگو. شناسههای ترتیبی (/share/1، /share/2) یا توکنهای قابلپیشبینی «با هرکسی که لینک دارد اشتراک بگذار» را به «با هرکسی که چند URL امتحان میکند اشتراک بگذار» تبدیل میکنند — توکن باید کل مرز امنیتی باشد، که یعنی واقعاً باید غیرقابلحدس باشد.
برگرداندن خطاهای متفاوت برای منقضیشده در برابر لغوشده در برابر ناموجود. شبیه تجربهی کاربری بهتر بهنظر میرسد («این لینک منقضی شده» در برابر «این لینک وجود ندارد») اما راهی هم هست برای تأیید اینکه یک توکن قبلاً معتبر بوده، که دقیقاً همان نوع اطلاعاتی است که یک لینک لغوشده نباید بتواند نشت دهد.
استفادهی مجدد از مدل داخلی برای پاسخ عمومی. فیلترکردن فیلدها هنگام سریالایز کار میکند تا وقتی کسی فیلدی به مدل داخلی اضافه کند و فهرست فیلتر را فراموش کند — یک نوع اختصاصیِ عمومی با نیازمندکردن اجازهی صریح برای هر فیلدی که فاش میکند، ایمن-بهشکست است.
سؤالات متداول
میتوانم لینکی بسازم که هرگز منقضی نشود؟
بله — ExpiresAt بهطور پیشفرض نال است، یعنی لینک تا وقتی کسی صراحتاً لغوش کند فعال میماند.
آیا دیدن بورد به هیچ کوکی یا نشست WKFGo نیاز دارد؟
نه — توکن داخل URL کل اعتبار است؛ هیچ وضعیت نشستی به بیننده وصل نیست.
آیا بینندهی لینک اشتراکگذاری میتواند مالی، اسناد یا ورودیهای پایگاهدانش را ببیند؟
نه — بار دادهی بورد عمومی فقط شامل عنوان تسک، برچسبها، پیشرفت، اولویت، تاریخ سررسید و اسمهای مسئولین است؛ هیچچیزی از مالی، اسناد یا دانش بخشی از این تصویر نیست.
جمعبندی
یک لینک اشتراکگذاری عمومی و یک حساب مهمان مسئلههای متفاوتی را حل میکنند — یکی توکن حامل برای یک نگاه سریع و ناشناس است؛ دیگری یک هویت محدود برای همکاری مداوم. ساختن نسخهی عمومی بهشکل واقعاً باریک و اختصاصی — با یک راز تصادفیِ واقعی و یک مدل امنیتی که از طریق پیامهای خطای متفاوت وضعیت نشت نمیدهد — همان چیزی است که «بهاشتراکگذاری راحت» را از «حدسزدن راحت» جدا نگه میدارد.