راه اشتباه اجرای یک سیستم نام کاربریِ چندمستأجری

اگر نام‌های کاربری در کل پایگاه‌داده‌تان به‌طور سراسری یکتا باشند، دو چیز همان لحظه‌ای که مشتری‌های واقعی دارید خراب می‌شود. اول، کسی در شرکت الف نمی‌تواند sara باشد چون کسی در شرکت ب از قبل هست — تصادفی که هیچ ربطی به هیچ‌کدام از دو شرکت ندارد، اما جلوی یک ثبت‌نام کاملاً معقول را می‌گیرد. دوم، و بدتر: یک چک یکتاییِ سراسری اطلاعات را بین مستأجرها نشت می‌دهد. تلاش برای ثبت sara و گرفتن «قبلاً گرفته شده» به شما می‌گوید یک sara جایی در سیستم وجود دارد — که بسته به چیز دیگری که می‌توانید استنتاج کنید، می‌تواند نشت دهد که آیا یک شرکت یا فرد خاص اصلاً از محصول استفاده می‌کند.

مدل WKFGo: نام‌های کاربری به سازمان محدودند

یک نام کاربری فقط باید درون سازمانش یکتا باشد، نه در کل پلتفرم:

func (s *userService) resolveLoginUser(company, username string) (*userModel.User, error) {
    if strings.TrimSpace(company) == "" {
        return s.userRepo.FindPlatformUserByUsername(username)
    }
    orgID, err := s.userRepo.FindOrgIDBySlugOrName(strings.TrimSpace(company))
    if err != nil || orgID == nil {
        return nil, ErrOrgNotFound
    }
    return s.userRepo.FindByUsernameInOrg(*orgID, username)
}

دو شرکت مختلف می‌توانند هرکدام sara، admin، support خودشان را داشته باشند — جفتِ (سازمان، نام کاربری) کلید یکتای واقعی است، نه نام کاربری به‌تنهایی. این فقط یک نکته‌ی تجربه‌ی کاربری نیست؛ این چیزی است که ثبت‌نام چندمستأجری را بدون اینکه هر مشتری جدید بر سر یک فضای‌نام مشترک مذاکره کند کارآمد می‌کند.

که یعنی ورود اول باید بداند کدام شرکت

اگر نام‌های کاربری به‌طور سراسری یکتا نباشند، «به‌عنوان sara وارد شو» یک سؤال مبهم است تا وقتی بدانید sara کدام سازمان. WKFGo این را با یک اسلاگ شرکت که در خود URL ورود پخته شده حل می‌کند: /login/acme_corp. اسلاگ از نام نمایشیِ شرکت مشتق می‌شود — فاصله‌ها به آندرلاین تبدیل می‌شوند، چون آندرلاین در نام‌های نمایشیِ شرکت مجاز نیست، که جداکننده را در هر دو جهت بدون ابهام نگه می‌دارد:

export function slugifyOrgName(name) {
  let s = String(name).trim().toLowerCase();
  s = s.replace(/[^a-z0-9؀-ۿ]+/g, '_');
  s = s.replace(/_+/g, '_').replace(/^_|_$/g, '');
  return s;
}

توجه کنید که صراحتاً کاراکترهای فارسی (بازه‌ی ؀-ۿ) را کنار الفبانویسیِ لاتین نگه می‌دارد — شرکتی که به فارسی نام‌گذاری شده اسلاگی خوانا در همان خط خودش می‌گیرد، نه یک URL خراب‌شده به آشغال درصدرمزگذاری‌شده یا مجبورشده به یک ترجمه‌ی فقط‌لاتین.

تنها پیامی که اجازه دارد خاص باشد

خطاهای ورود تقریباً همه‌جا عمداً عمومی‌اند — رمز عبور اشتباه، نام کاربری ناشناخته، و یک حساب ایجنتِ غیرفعال همه در همان پاسخ «نام کاربری یا رمز عبور اشتباه است» فرو می‌ریزند، پس هیچ‌کدام از این موارد را کسی که فرم ورود را کاوش می‌کند نمی‌تواند تشخیص دهد. اما یک اسلاگ شرکتِ ناشناخته خطای خودش، متمایز، را می‌گیرد:

if errors.Is(err, ErrOrgNotFound) {
    return nil, err
}

این یک استثنای عمدی و باریک برای «همیشه عمومی باش» است. اسلاگ شرکت یک راز نیست — قرار است به‌اشتراک گذاشته شود، بوکمارک شود، و توسط کارمندی که ممکن است کمی اشتباه تایپش کند تایپ شود (acme-corp به‌جای acme_corp، یا یک نام قدیمیِ شرکت). گفتن به آن فرد «هیچ سازمانی برای این URL ورود پیدا نشد» یک اصلاح مستقیمِ تجربه‌ی کاربری برای یک اشتباه تایپیِ URL است. فروپاشاندن آن در همان پیام عمومیِ «رمز عبور اشتباه» یک اشتباه بی‌ضررِ URL را از یک مشکل واقعیِ اعتبار غیرقابل‌تشخیص می‌کرد، که به هیچ‌کس کمکی نمی‌کند — درحالی‌که خودِ اعتبارها پشت همان یک پیام عمومی می‌مانند، چون آن مرزی است که واقعاً ارزش محافظت دارد.

این برای حساب‌های سطح‌پلتفرم چه معنایی دارد

به شاخه‌ی بالای resolveLoginUser توجه کنید: یک رشته‌ی خالیِ شرکت کاملاً از جستجوی سازمان می‌گذرد و یک کاربر پلتفرم را با نام کاربری جستجو می‌کند — این مسیر برای ادمین‌های SaaS و حساب‌های سطح‌پلتفرم است که به فضای‌نام هیچ مستأجر خاصی محدود نیستند. دو مسیر جستجوی جدا، دو حوزه‌ی یکتاییِ جدا: حساب‌های پلتفرم در یکی، اعضای هر سازمان در فضای‌نام خودشان.

اشتباهات رایج

یکتاکردن نام‌های کاربری به‌طور سراسری «برای سادگی». ساختش ساده‌تر است، اما اصطکاک ثبت‌نامِ مصنوعی بین مشتری‌های نامرتبط ایجاد می‌کند و اطلاعات وجودِ بین‌مستأجری را از طریق خطاهای معمولیِ «گرفته شده» نشت می‌دهد.

عمومی‌بودن درباره‌ی شرکت-پیدا-نشد به همان شکل که درباره‌ی اعتبار عمومی هستید. ریسک یکسانی ندارند — اسلاگ شرکت عمومی و مستعد اشتباه تایپی است؛ فروپاشاندنش در یک خطای عمومیِ حساس-به-امنیت فقط اشتباهات معمولیِ URL را بدون هیچ فایده‌ی امنیتی سخت‌تر برای خودتشخیصی می‌کند.

ترجمه‌ی نام‌های غیرلاتینِ شرکت به اسلاگ‌های ASCII. URL‌های غیرقابل‌خواندن برای همان آدم‌هایی تولید می‌کند که واقعاً باید تایپشان کنند؛ نگه‌داشتن خط اصلی در اسلاگ (جایی که مجموعه‌کاراکتر اجازه می‌دهد) هم درست‌تر و هم قابل‌استفاده‌تر است.

سؤالات متداول

می‌توانم اسلاگ ورود شرکتم را بعداً تغییر بدهم؟

اسلاگ از نام نمایشیِ سازمان مشتق می‌شود، پس تغییرنام سازمان اسلاگ را تغییر می‌دهد — URL‌های ورود بوکمارک‌شده‌ی قدیمی نیاز به به‌روزرسانی دارند.

اگر دو شرکت نام‌های خیلی مشابهی انتخاب کنند که به یک رشته اسلاگ می‌شوند چه؟

یکتاییِ اسلاگ در سطح سازمان اجرا می‌شود؛ یک تداخل همان‌طور مدیریت می‌شود که هر تداخل محدودیت یکتا مدیریت می‌شود — سازمان دوم نمی‌تواند یک اسلاگ یکسان را ادعا کند.

آیا این روی احراز کلید API یا MCP اثر می‌گذارد؟

نه — کلیدهای API شخصی (wk_…) از قبل زمینه‌ی سازمان را داخل خود کلید پخته دارند، پس به مرحله‌ی جداگانه‌ی اسلاگ شرکت که ورود تعاملیِ نام‌کاربری/رمزعبور نیاز دارد نیازی ندارند.

جمع‌بندی

در یک محصول چندمستأجری، «یکتا» تقریباً هیچ‌وقت به‌معنای «یکتا در کل مشتری‌هایی که تا ابد خواهیم داشت» نیست — به‌معنای یکتا درون مرزی است که واقعاً اهمیت دارد، که برای یک نام کاربری، سازمان است، نه پلتفرم. درست‌کردن این محدوده هم از یک دعوای فضای‌نامِ مصنوعی بین مشتری‌های نامرتبط جلوگیری می‌کند، هم از یک راه ساکت برای نشت‌دادن اینکه چه‌کسی دیگر از محصولتان استفاده می‌کند.