خلاصهی جواب: خطای 419 لاراول یعنی CSRF token که با فرم یا درخواست AJAX فرستادی با token داخل session کاربر یکی نیست. اگه روی local همهچیز درسته و فقط روی سرور 419 میگیری، تقریباً همیشه مشکل از session و کوکیه، نه از کدت: APP_URL و HTTPS با هم نمیخونن، Laravel نمیفهمه پشت proxy روی HTTPS هستی، SESSION_DOMAIN یا SESSION_SECURE_COOKIE اشتباه ست شده، session بین چند سرور مشترک نیست، یا یه کش صفحه داره HTML قدیمی (با token قدیمی) تحویل میده. توی این مقاله اول دو سه مورد سادهی local رو سریع رد میکنیم، بعد میریم سراغ شاخهی production و Livewire.
این مقاله به درد کی میخوره؟
اگه:
- با Laravel 10 به بالا کار میکنی و
@csrfو Blade رو میشناسی، - فرم یا کامپوننت Livewireت روی local کار میکنه،
- ولی روی سرور واقعی صفحهی
419 Page Expiredیا پیام «This page has expired» Livewire رو میبینی،
دقیقاً جای درستی هستی. اگه روی local هم 419 میگیری، از بخش «شاخهی ساده» شروع کن؛ اگه فقط روی production مشکل داری، مستقیم برو سراغ «شاخهی production».
CSRF token چطور به session وصله؟
طبق مستندات رسمی Laravel دربارهی CSRF:
Laravel automatically generates a CSRF "token" for each active user session managed by the application. This token is used to verify that the authenticated user is the person actually making the requests to the application. Since this token is stored in the user's session and changes each time the session is regenerated, a malicious application is unable to access it.
پس token به session گره خورده و session هم به کوکی. مسیر یه درخواست توی production معمولاً این شکلیه:
Browser
│ Cookie: laravel_session=... + XSRF-TOKEN=...
↓
CDN / Cloudflare (اختیاری)
↓
Nginx (پایان HTTPS، یا reverse proxy)
↓
PHP-FPM → middleware گروه web
↓
StartSession → خواندن session از روی کوکی
↓
ValidateCsrfToken (Laravel 11/12) / PreventRequestForgery (Laravel 13)
↓
مقایسهی token درخواست با token داخل session → OK یا 419
هر جای این مسیر که کوکی session به سرور نرسه، session عوض بشه، یا HTML صفحه token یه session دیگه رو داشته باشه، نتیجه 419 است.
شاخهی ساده: @csrf و AJAX (معمولاً روی local)
سناریوی A: @csrf توی فرم جا افتاده
{{-- ❌ اشتباه: 419 --}}
<form method="POST" action="/profile">
<input type="text" name="name">
<button type="submit">ارسال</button>
</form>
{{-- ✅ درست --}}
<form method="POST" action="/profile">
@csrf
<input type="text" name="name">
<button type="submit">ارسال</button>
</form>
@csrf در واقع همین فیلد مخفی رو میسازه:
<input type="hidden" name="_token" value="{{ csrf_token() }}">
سناریوی B: درخواست fetch بدون token
// ❌ اشتباه: 419
fetch('/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'ali' })
});
// ✅ درست: token از meta tag
fetch('/profile/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ name: 'ali' })
});
برای اینکه JavaScript به token دسترسی داشته باشه، توی <head> این meta tag رو بذار. @csrf فقط داخل فرم کار میکنه و برای AJAX کافی نیست:
<head>
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
یه نکته: routeهای routes/api.php بهصورت پیشفرض middleware گروه web (و در نتیجه CSRF) رو ندارن. 419 مال routeهای گروه web است.
سناریوی C: Axios
طبق مستندات Laravel، Axios روی درخواستهای same-origin خودش مقدار کوکی XSRF-TOKEN رو میخونه و توی هدر X-XSRF-TOKEN میفرسته، پس معمولاً لازم نیست کاری بکنی. اگه درخواست cross-origin داری (مثلاً فرانت روی یه subdomain دیگه)، باید کوکیها رو هم بفرستی:
// resources/js/bootstrap.js
axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true; // Axios 1.6 به بالا، برای cross-origin
شاخهی production: APP_URL، HTTPS و proxy
APP_URL باید با آدرس واقعی سایت یکی باشه
# ❌ اشتباه: سایت روی https است ولی APP_URL روی http
APP_URL=http://example.com
# ✅ درست
APP_URL=https://example.com
APP_URL مستقیماً کوکی رو نمیسازه، ولی Laravel توی ساخت URLها (مثلاً از CLI، صفها و بعضی پکیجها) ازش استفاده میکنه و ناهماهنگیاش با آدرس واقعی، سرنخ اول خیلی از مشکلات http/https است. بعد از عوض کردن .env هم یادت نره php artisan config:cache رو دوباره بزنی، وگرنه مقدار قدیمی از کش خونده میشه. اگه deploy رو با GitHub Actions خودکار کردی (راهنمای دیپلوی خودکار لاراول با GitHub Actions)، مطمئن شو این مرحله بعد از نوشتن .env اجرا میشه.
Laravel باید بفهمه درخواست HTTPS بوده (trustProxies)
خیلی وقتها HTTPS روی CDN یا Nginx تموم میشه و درخواست با HTTP به اپ میرسه:
Browser (HTTPS) → CDN یا Nginx (HTTPS) → اپ Laravel (HTTP)
اگه Laravel به proxy اعتماد نکنه، فکر میکنه درخواست HTTP بوده. نتیجه؟ url()، route() و asset() آدرس http:// میسازن؛ مثلاً action فرم یا آدرس endpoint لایوایر http:// میشه. حالا اگه SESSION_SECURE_COOKIE=true باشه، مرورگر کوکی Secure رو روی درخواست http:// نمیفرسته، session جدید ساخته میشه و 419 میگیری. اگه هم سرور http رو به https ریدایرکت کنه، درخواست POST وسط ریدایرکت از دست میره.
اگه Nginx جلوی یه سرور یا کانتینر دیگه نشسته، هدرهای forwarded رو بفرسته:
location / {
proxy_pass http://laravel_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme; # ← کلیدی
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
و توی Laravel 11 به بالا (bootstrap/app.php):
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*'); // یا فقط IP/رنج proxyها
})
⚠️ at: '*' فقط وقتی امنه که PHP مستقیم از اینترنت در دسترس نباشه؛ وگرنه هر کسی میتونه هدر X-Forwarded-Proto جعل کنه. اگه میتونی، IP یا رنج proxyهات رو دقیق بنویس.
توی Laravel 13 این موضوع یه دلیل دیگه هم داره: طبق مستندات Laravel 13، چک origin با هدر Sec-Fetch-Site فقط روی اتصال امن انجام میشه. اگه Laravel درخواست رو HTTP ببینه، این لایه رد میشه و همهچیز میافته روی چک token معمولی.
Cloudflare با SSL حالت Flexible
توی حالت Flexible، Cloudflare با مرورگر HTTPS حرف میزنه ولی با سرور تو HTTP:
Browser (HTTPS) → Cloudflare (HTTPS) → Origin (HTTP)
این همون سناریوی بالاست، فقط یه لایه بیشتر. راهحل: حالت SSL رو بذار روی Full (strict) و روی origin هم گواهی داشته باش، و trustProxies رو درست تنظیم کن.
یه سناریوی رایج: سایت تازه پشت یه CDN یا proxy (مثلاً Cloudflare) رفته و یکدفعه همهی فرمهای Livewire شروع میکنن به 419 دادن، در حالی که هیچ کدی عوض نشده. تقریباً همیشه علت همینه که trustProxies تنظیم نشده و Laravel درخواست رو HTTP میبینه، و ترکیبش با SESSION_SECURE_COOKIE=true باعث میشه کوکی session نرسه. برای همین بعد از هر تغییر در CDN یا proxy، این دوتا اولین چیزهاییان که باید چک کنی.
شاخهی production: کوکی session (Domain، Secure، SameSite)
SESSION_DOMAIN و subdomain
# پیشفرض: کوکی فقط برای همون host که صفحه ازش اومده
SESSION_DOMAIN=null
# اگه session باید بین app.example.com و admin.example.com مشترک باشه
SESSION_DOMAIN=.example.com
نکتهی مهم که زیاد اشتباه گفته میشه: وقتی Domain کوکی رو ست میکنی، چه example.com باشه چه .example.com، مرورگرهای امروزی کوکی رو برای همهی subdomainها هم میفرستن (نقطهی اول رو نادیده میگیرن). فقط وقتی SESSION_DOMAIN خالی (null) باشه، کوکی host-only است. پس:
- اگه
SESSION_DOMAINرو روی دامنهای گذاشتی که با دامنهی واقعی سایت نمیخونه (مثلاً مقدار staging روی production مونده)، مرورگر کوکی رو قبول نمیکنه و هر درخواست یه session جدید میسازه → 419. - اگه یه کوکی session قدیمی host-only و یه کوکی جدید با Domain هماسم داری، مرورگر ممکنه هر دو رو بفرسته و Laravel اشتباهی رو بخونه. بعد از عوض کردن
SESSION_DOMAIN، اسم کوکی (SESSION_COOKIE) رو هم عوض کن یا کوکیها رو پاک کن.
طبق مستندات Laravel دربارهی session، تنظیمات کوکی session همه توی config/session.php هستن و از .env خونده میشن.
SESSION_SECURE_COOKIE و SESSION_SAME_SITE
ترکیب درست برای سایتی که فقط HTTPS است:
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax
SESSION_HTTP_ONLY=true
دو حالتی که به 419 میرسن:
SESSION_SECURE_COOKIE=trueروی سایتی که هنوز با HTTP باز میشه (مثلاً staging بدون SSL): مرورگر کوکی Secure رو روی HTTP ذخیره نمیکنه، پس هر فرم 419 میده.SESSION_SAME_SITE=noneبدونSecure: مرورگرهای مدرن کوکی رو رد میکنن.
SESSION_SECURE_COOKIE=false روی HTTPS معمولاً خودش 419 نمیسازه، ولی یه ضعف امنیتیه. طبق مقالهی StackShield دربارهی امنیت session لاراول، فلگ Secure باعث میشه کوکی فقط روی HTTPS فرستاده بشه و روی شبکههای عمومی قابل شنود نباشه.
طبق همون منبع، SameSite سه حالت داره:
| مقدار | رفتار | کِی استفاده کنیم |
|---|---|---|
lax (پیشفرض Laravel) |
روی ناوبری top-level فرستاده میشه، روی POST بینسایتی نه | بیشتر اپها |
strict |
روی هیچ درخواست بینسایتی فرستاده نمیشه | اپهای حساس؛ ولی کاربری که از لینک ایمیل میاد لاگیننشده دیده میشه |
none |
همیشه فرستاده میشه، حتماً با Secure |
iframe یا سناریوی واقعاً cross-site |
SESSION_HTTP_ONLY بهصورت پیشفرض true است؛ عوضش نکن، وگرنه JavaScript (و هر XSS) به کوکی session دسترسی پیدا میکنه.
شاخهی production: driver session، چند سرور و کش صفحه
session روی چند سرور یا کانتینر
توی discussion شمارهی 4799 مخزن Livewire یکی از کاربرها گزارش داده که روی Kubernetes با دو pod و SESSION_DRIVER=file، 419 میگرفته؛ چون session روی دیسک یه pod ذخیره میشد و درخواست بعدی به pod دیگه میرفت. همین برای چند کانتینر PHP پشت load balancer هم صدق میکنه. راهحل: driver مشترک مثل redis یا database.
اگه تکسرور هستی و driver روی file است، مطمئن شو storage/framework/sessions قابل نوشتن برای کاربر PHP-FPM است.
کش صفحهی کامل یا CDN
این یکی کمتر گفته میشه: اگه برای سرعت (که توی راهنمای سئو تکنیکال لاراول هم بهش پرداختم) کش صفحهی کامل گذاشتی، یا CDN صفحههای HTML رو کش میکنه، کاربر HTMLی رو میگیره که csrf_token() یه session دیگه داخلشه. فرم رو که بفرسته، 419. صفحههایی که فرم یا کامپوننت Livewire دارن رو از کش کامل مستثنی کن، یا token رو بعد از لود با یه درخواست جدا تازه کن.
عمر session
اگه کاربر صفحه رو باز گذاشته و بعد از مدتی فرم رو میفرسته، ممکنه session منقضی شده باشه. پیشفرض SESSION_LIFETIME توی Laravel ۱۲۰ دقیقه است. برای این حالت یه صفحهی resources/views/errors/419.blade.php دوستانه بساز که بگه «صفحه رو رفرش کن».
جدول تشخیص: ۸ سناریو و روش چک کردن
| # | سناریو | علامت | روش چک | نتیجهی سالم |
|---|---|---|---|---|
| ۱ | @csrf توی فرم نیست |
فرم همیشه 419 | View Source فرم | <input name="_token"> هست |
| ۲ | meta tag csrf-token نیست |
فقط AJAX 419 | View Source → <head> |
<meta name="csrf-token"> هست |
| ۳ | APP_URL اشتباه |
لینکها/ریدایرکتها http:// |
php artisan tinker --execute="echo config('app.url');" |
https://... |
| ۴ | Laravel HTTPS رو نمیبینه | action فرم یا URL لایوایر http:// |
View Source، یا یه route موقت که request()->isSecure() رو برگردونه (توی tinker همیشه false است) |
true و URLهای https:// |
| ۵ | SESSION_DOMAIN اشتباه |
کوکی session ذخیره نمیشه | DevTools → Application → Cookies | کوکی با Domain درست هست |
| ۶ | Secure روی HTTP | روی staging بدون SSL همهچیز 419 | DevTools → Cookies (ستون Secure) و آدرس صفحه | Secure فقط روی HTTPS |
| ۷ | session مشترک نیست / permission | هر درخواست session_id جدید | SESSION_DRIVER و تعداد سرورها؛ لاگ permission روی storage/framework/sessions |
driver مشترک، بدون خطای permission |
| ۸ | HTML کششده / session منقضی | 419 بعد از مدتی یا برای همهی کاربرها | هدرهای پاسخ (Cache-Control، هدر کش CDN)، config('session.lifetime') |
صفحهی فرمدار کش نمیشه |
Livewire: چرا 419 میده؟
Livewire خودش token رو میفرسته: اسکریپتی که @livewireScripts (یا تزریق خودکار در Livewire 3) توی صفحه میذاره، csrf_token() همون لحظه رو داره و همهی درخواستهای wire:click و wire:submit با اون فرستاده میشن. پس برای اکشنهای Livewire لازم نیست @csrf بذاری؛ اگه 419 میگیری، تقریباً همیشه یکی از همون مشکلات session بالاست. وقتی 419 بیاد، Livewire پیام «This page has expired» رو نشون میده و پیشنهاد رفرش میکنه.
بقیهی راهحلهایی که توی discussion شمارهی 4799 جواب داده بودن:
- بعد از آپدیت Livewire، اگه assetهاش رو publish کردی، دوباره publish کن (
php artisan livewire:publish --assets)؛ JS قدیمی با بکاند جدید جور نیست. SESSION_DOMAINرو چک کن که کوکی session واقعاً ذخیره میشه.- اگه routeهای Livewire رو توی یه پکیج تعریف کردی، middleware گروه
webرو داشته باشن.
بعضیها توی همون discussion پیشنهاد دادن
livewire/*رو از CSRF مستثنی کنی. این کار رو نکن؛ مشکل رو پنهان میکنه و همهی اکشنهای Livewire رو بیدفاع میذاره.
Laravel 13: محافظت CSRF با چک origin
طبق مستندات Laravel 13، middleware حالا PreventRequestForgery است و دو لایه داره: اول هدر Sec-Fetch-Site رو چک میکنه و اگه درخواست same-origin باشه، بدون چک token قبولش میکنه؛ اگه این چک ممکن نباشه (مرورگر قدیمی، یا اتصال غیرامن)، میره سراغ چک token معمولی.
پس اگه روی Laravel 13 هستی و 419 میگیری:
- توی تب Network مقدار
Sec-Fetch-Siteدرخواست رو ببین. اگهcross-siteیاsame-siteاست، درخواست از یه origin دیگه (subdomain، iframe) میاد و token لازمه. - برای subdomainها،
preventRequestForgery(allowSameSite: true)رو در نظر بگیر. - اگه Laravel درخواست رو HTTP میبینه (بخش trustProxies)، لایهی origin کلاً اجرا نمیشه.
یه نکته: توی حالت originOnly: true، درخواست ردشده بهجای 419، کد 403 میگیره.
چرا نباید CSRF رو خاموش کنی؟
هیچوقت این کار رو نکن:
// ❌ هرگز
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['*']);
})
با این کار یه سایت دیگه میتونه از طرف کاربر لاگینشدهی تو فرم بفرسته (عوض کردن ایمیل، حذف حساب و…).
استثنای درست: webhook
فقط برای webhookهای سرویسهای بیرونی که token ندارن (Laravel 11 و 12؛ توی Laravel 13 اسم متد preventRequestForgery است):
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'stripe/*',
'webhooks/*',
]);
})
طبق مستندات Laravel:
Sometimes you may wish to exclude a set of URIs from CSRF protection. For example, if you are using Stripe to process payments and are utilizing their webhook system, you will need to exclude your Stripe webhook handler route from CSRF protection since Stripe will not know what CSRF token to send to your routes.
⚠️ حتی وقتی webhook رو مستثنی کردی، امضاش (مثل هدر Stripe-Signature) رو حتماً verify کن.
سوالات متداول
۱. فرق ValidateCsrfToken و PreventRequestForgery چیه؟
ValidateCsrfToken اسم middleware توی Laravel 11 و 12 است و فقط token رو چک میکنه. توی Laravel 13 اسمش PreventRequestForgery شده و قبل از token، هدر Sec-Fetch-Site رو هم چک میکنه.
۲. چرا روی local کار میکنه ولی روی production نه؟
چون روی local معمولاً یه دامنه، HTTP ساده و یه سرور داری. روی production پای HTTPS، proxy یا CDN، subdomain، چند سرور و کش وسط میاد. رایجترینها: trustProxies تنظیم نشده، SESSION_DOMAIN یا SESSION_SECURE_COOKIE اشتباه، driver session غیرمشترک، و HTML کششده.
۳. میتونم CSRF رو فقط برای یه route خاموش کنم؟
آره، با validateCsrfTokens(except: [...]) (یا preventRequestForgery(except: [...]) توی Laravel 13). ولی فقط برای webhook و endpointهای سرویسهای بیرونی، هیچوقت برای routeهایی که کاربر باهاشون کار میکنه.
۴. کوکیها رو کجای DevTools ببینم؟
Chrome: کلید F12 → Application → Storage → Cookies. Firefox: کلید F12 → Storage → Cookies. Safari: Develop → Show Web Inspector → Storage → Cookies.
۵. Livewire 3 روی production 419 میده؛ از کجا شروع کنم؟
۱) View Source بگیر و ببین آدرس endpoint لایوایر https:// است یا نه (trustProxies). ۲) توی DevTools ببین کوکی session ذخیره میشه یا نه (SESSION_DOMAIN و SESSION_SECURE_COOKIE). ۳) اگه چند سرور داری، driver session مشترک باشه. ۴) صفحه از کش کامل یا CDN نیاد.
۶. چرا Livewire گاهی با token درست هم 419 میده؟
چون token درسته ولی مال session دیگهایه: session منقضی شده، درخواست به سرور دیگهای رفته که session رو نداره، یا HTML از کش اومده. اول این سه تا رو چک کن.
اگه دو شاخهی بالا رو جدا جدا بری و جدول تشخیص رو قدمبهقدم چک کنی، معمولاً خیلی زود معلوم میشه session کجای مسیر گم میشه. و یادت باشه: راهحل 419 هیچوقت خاموش کردن CSRF نیست؛ این یکی از اصلیترین لایههای دفاعی اپلیکیشنته.
هنوز 419 داری یا وقت دیباگش رو نداری؟ متن خطا، .env (بدون کلیدها و پسوردها) و تنظیم Nginx رو به نیکس، دستیار هوش مصنوعی برنامهنویسی کاپیتان کد بده تا قدمبهقدم باهات دیباگش کنه. اگه هم ترجیح میدی کل کار دست یه نفر باشه، از طراحی و توسعهی سایت اختصاصی با لاراول تا امنیت و نگهداری سایت رو میتونی به کاپیتان کد بسپاری.
اگه تازه وارد دنیای بکاند شدی و میخوای بدونی session و کوکی و سرور کجای مسیر یادگیریان، مقایسهی فرانتاند، بکاند و فولاستک و بهترین زبانهای برنامهنویسی برای یادگیری در ۲۰۲۶ رو هم بخون.
موفق باشی. اگه سوالی داشتی یا گیر کردی، تو کامنتها بپرس.