کوکی‌ها و Session‌ها

فینچ دو مکانیزم برای نگه‌داری داده بین درخواست‌های HTTP فراهم می‌کند: کوکی‌ها (سمت کلاینت) و Session‌ها (سمت سرور). درک تفاوت این دو به شما کمک می‌کند ابزار مناسب را انتخاب کنید:

  • کوکی‌ها در مرورگر ذخیره می‌شوند و با هر درخواست ارسال می‌شوند. آن‌ها برای ترجیحات سبک کاربر (theme، زبان) که می‌توان با خیال راحت سمت کلاینت ذخیره کرد مناسب‌اند.
  • Session‌ها روی سرور ذخیره می‌شوند. فقط یک session ID به مرورگر ارسال می‌شود. آن‌ها برای داده‌های حساس مانند کاربر جاری که وارد شده مناسب‌اند.

هر دو از طریق شیء rq درون هر کنترلر یا route handler در دسترس هستند.

کوکی‌ها

کوکی‌ها جفت‌های رشته‌ای key-value هستند که در مرورگر ذخیره می‌شوند. فینچ آن‌ها را از هدرهای ورودی Cookie می‌خواند و از طریق هدرهای پاسخ Set-Cookie می‌نویسد.

خواندن یک کوکی

برای خواندن یک کوکی از rq.getCookie(key, def: defaultValue) استفاده کنید. پارامتر def مشخص می‌کند در صورت عدم وجود کوکی چه چیزی بازگردانده شود.

safe در هر دوی getCookie و addCookie به‌صورت پیش‌فرض true است. مگر اینکه خلاف آن را مشخص کنید، هر کوکی‌ای که فینچ می‌خواند یا می‌نویسد به‌صورت شفاف رمزگذاری/رمزگشایی می‌شود. برای ذخیره یا خواندن یک مقدار واقعاً plain-text (قابل‌خواندن در dev tools مرورگر)، باید صراحتاً safe: false پاس دهید:

// رمزگذاری‌شده (پیش‌فرض) — هنگام خواندن دوباره با getCookie به‌صورت خودکار رمزگشایی می‌شود
var token = rq.getCookie('auth_token', def: '');

// ساده (plain) — مقدار دقیقاً همان‌طور که داده شده ذخیره و بازگردانده می‌شود
var theme = rq.getCookie('theme', def: 'light', safe: false);

نوشتن یک کوکی

برای تنظیم یک کوکی از rq.addCookie(key, value) استفاده کنید. کوکی به پاسخ ضمیمه می‌شود:

// رمزگذاری‌شده (پیش‌فرض)
rq.addCookie('auth_token', 'abc123');

// ساده (plain)
rq.addCookie('theme', 'dark', safe: false);

addCookie همچنین گزینه‌های دقیق‌تری را برای هدر زیرین Set-Cookie می‌پذیرد:

rq.addCookie(
  'session_pref',
  'compact',
  safe: false,
  duration: Duration(days: 30), // Max-Age را تنظیم می‌کند
  path: '/',                    // پیش‌فرض '/' است
  domain: '.example.com',
  secure: true,                 // فقط از طریق HTTPS ارسال می‌شود
  httpOnly: true,                // از JavaScript قابل‌خواندن نیست
  sameSite: SameSite.lax,
);

حذف یک کوکی

برای حذف یک کوکی، فینچ آن را با یک max-age منقضی‌شده دوباره تنظیم می‌کند:

rq.removeCookie('theme');

مثال کامل

class CookieController extends Controller {
  Future<String> showCookie() async {
    var value = rq.getCookie('test', def: 'not set', safe: false);
    rq.addParam('cookieValue', value);
    return rq.renderView(path: 'example/cookie');
  }

  Future<String> addCookie() async {
    var key   = rq.get<String>('key', def: '');
    var value = rq.get<String>('value', def: '');
    var safe  = rq.get<bool>('safe', def: false);

    if (key.isNotEmpty) {
      rq.addCookie(key, value, safe: safe);
    }
    return rq.redirect('/example/cookie');
  }
}

Session‌ها

Session‌ها توسط HttpSession داخلی Dart مدیریت می‌شوند. هر بازدیدکننده یک session ID منحصربه‌فرد دریافت می‌کند که در یک کوکی مرورگر ذخیره می‌شود، اما داده واقعی روی سرور در حافظه (memory) قرار دارد.

مهم: HttpSession پیش‌فرض Dart memory-backed است. داده‌های session با ری‌استارت سرور از بین می‌روند و بین چندین نمونه سرور پشت یک load balancer به اشتراک گذاشته نمی‌شوند. برای استفاده در production در مقیاس بزرگ، یک session store پایدار/مشترک را در نظر بگیرید یا داده‌های حیاتی را در عوض در دیتابیس ذخیره کنید.

خواندن یک مقدار Session

برای خواندن یک مقدار از rq.getSession(key, def: defaultValue) استفاده کنید. اگر کلید وجود نداشته باشد، def را برمی‌گرداند. توجه کنید که نوع بازگشتی Object است، نه String — اگر به یک نوع مشخص نیاز دارید آن را cast کنید:

var userEmail = rq.getSession('user', def: '') as String;

نوشتن یک مقدار Session

برای ذخیره یک مقدار در session جاری از rq.addSession(key, value) استفاده کنید. مقدار می‌تواند هر نوع JSON-friendly باشد (String، num، bool، Map، List)، نه فقط رشته:

rq.addSession('user', '[email protected]');
rq.addSession('cart', {'items': 3, 'total': 42.5});

حذف یک مقدار Session

rq.session.remove('user');

مثال کامل

یک جریان معمول ورود/خروج/داشبورد با استفاده از session‌ها:

class SessionController extends Controller {
  Future<String> login() async {
    var email    = rq.get<String>('email', def: '');
    var password = rq.get<String>('password', def: '');

    if (isValidUser(email, password)) {
      // ذخیره ایمیل کاربر در session پس از احراز هویت موفق
      rq.addSession('user', email);
      return rq.redirect('/dashboard');
    }
    return rq.renderError(401);
  }

  Future<String> logout() async {
    // حذف کاربر از session برای خروج (logout) او
    rq.session.remove('user');
    return rq.redirect('/login');
  }

  Future<String> dashboard() async {
    var email = rq.getSession('user', def: '') as String;

    // اگر session وجود ندارد، کاربر وارد نشده است — هدایت مجدد به صفحه ورود
    if (email.isEmpty) return rq.redirect('/login');

    rq.addParam('email', email);
    return rq.renderView(path: 'pages/dashboard');
  }
}

برای یک جریان کامل احراز هویت همراه با permissions و محافظت از مسیر، به Auth Controller مراجعه کنید — این بخش بر پایه همان فراخوانی‌های getSession/addSession که اینجا نشان داده شد بنا شده است.

نحوه عملکرد رمزگذاری کوکی

کلید رمزگذاری FinchConfigs.cookiePassword است:

FinchConfigs configs = FinchConfigs(
  cookiePassword: env.get('COOKIE_SECRET', 'change-this-in-production'),
);
// نوشتن — به‌صورت رمزگذاری‌شده در مرورگر ذخیره می‌شود
rq.addCookie('prefs', '{"theme":"dark"}');

// خواندن دوباره — فینچ آن را به‌صورت خودکار رمزگشایی می‌کند
var prefs = rq.getCookie('prefs', def: '{}');

از آنجا که رمزگذاری به cookiePassword گره خورده است، تغییر آن مقدار تمام کوکی‌های از پیش ذخیره‌شده در مرورگر بازدیدکنندگان را نامعتبر می‌کند — getCookie در رمزگشایی آن‌ها شکست می‌خورد و به‌طور خاموش (silently) به def بازمی‌گردد.

امنیت: از یک cookiePassword طولانی و تصادفی استفاده کنید. آن را به‌صورت یک متغیر محیطی (environment variable) ذخیره کنید. هرگز آن را hardcode نکنید یا در version control کامیت نکنید.

خواندن کوکی‌ها در قالب‌ها

همچنین می‌توانید مقدار یک کوکی را مستقیماً درون یک قالب Jinja بخوانید:

{{ $e.getCookie('theme', 'light') }}

برای دیدن هر چیز دیگری که روی $e درون یک قالب در دسترس است، به Template مراجعه کنید.