انتقال کاربرهای وردپرس به لاراول بدون ریست پسورد
خلاصهٔ کاملتر
نویسنده یه سایت وردپرس رو اول با نسخهٔ 6.7.9 و بعد با 7.1.2 بالا آورد و هشت کاربر ساخت. بعد هشهاشون رو دستنخورده به یه اپ Laravel 13.33 روی PHP 8.5 برد. هفت تا از هشت لاگین خطای This password does not use the Bcrypt algorithm دادن، چه با پسورد درست چه غلط. با اینکه وردپرس از نسخهٔ 6.8 از bcrypt استفاده میکنه، به دو دلیل جدا کار نمیکنه و یکیشون اصلاً خطا نمیده.
جدول wp_users میتونه چند فرمت با هم داشته باشه: هشهای phpass که با $P$ شروع میشن (قبل از 6.8)، هشهای $wp$2y$ (6.8 به بعد)، MD5 سیودو کاراکتری که ادمین دستی ست کرده، و $2y$ خالص از پلاگین قدیمی Roots. ارتقای وردپرس هشهای قدیمی رو دست نمیزنه و فقط وقتی کاربر دوباره لاگین کنه عوضش میکنه. نویسنده پیشنهاد میده قبل از نوشتن کد، با یه کوئری SQL تعداد هر فرمت رو بشمری. مشتریهای WooCommerce هم تو همین جدول هستن.
مشکل اول بیصداست: اگه با User::create() وارد کنی، cast از نوع hashed روی فیلد password (یعنی castی که مقدار رو خودکار هش میکنه، مگه اینکه از قبل هش باشه) فرمتهای وردپرس رو نمیشناسه. پس خود رشتهٔ هش رو دوباره bcrypt میکنه و کاربر دیگه هیچوقت لاگین نمیکنه. راهحل اینه که ردیفها رو با query builder وارد کنی که به castها کاری نداره:
DB::table('users')->insert([
'name' => $row->display_name,
'email' => $row->user_email,
'password' => $row->user_pass, // copied as is; the hashed cast would hash it again
]);مشکل دوم اینه که BcryptHasher لاراول قبل از مقایسه، الگوریتم هش رو چک میکنه و روی فرمت ناآشنا exception میده. حذف پیشوند $wp هم جواب نمیده، چون وردپرس خود پسورد رو bcrypt نمیکنه. اول یه HMAC-SHA384 با کلید wp-sha384 ازش میگیره و نتیجه رو base64 میکنه تا پسوردهای بلندتر از ۷۲ بایت بریده نشن. گذاشتن HASH_VERIFY=false هم فقط exception رو به false تبدیل میکنه و چک الگوریتم رو برای کل اپ برمیداره.
راهحل نویسنده یه کلاس WordPressHasher هست که از BcryptHasher ارث میبره. برای $wp همون HMAC و password_verify رو انجام میده، برای $P$ از کلاس PasswordHash خود وردپرس (فایل class-phpass.php که public domain هست) استفاده میکنه، و برای MD5 هم مقایسهٔ ساده. هر هش دیگهای به کلاس والد میرسه، و needsRehash() برای همهٔ فرمتهای وردپرس true برمیگردونه. این driver با Hash::extend() ثبت میشه و با HASH_DRIVER=wordpress تو .env فعال میشه.
با این hasher هر هشت لاگین موفق شد و چون از Laravel 11 به بعد SessionGuard::attempt() خودش rehash میکنه، بعد از اولین لاگین درست همهٔ هشها به $2y$12$ تبدیل شدن. اگه لاگین رو دستی با Hash::check() انجام میدی (مثلاً تو API) یا rehash_on_login رو خاموش کردی، باید خودت Hash::needsRehash() رو صدا بزنی. یه تفاوت کوچیک هم هست: وردپرس فاصلههای اول و آخر پسورد رو trim میکنه، ولی بعد از rehash لاراول رشتهٔ دقیق رو مقایسه میکنه.
نکات کلیدی:
- wp_users میتونه چهار فرمت داشته باشه: $P$ (۳۴ کاراکتر)، $wp$2y$ (۶۳ کاراکتر)، MD5 (۳۲ کاراکتر) و $2y$ خالص (۶۰ کاراکتر).
- User::create() بهخاطر cast از نوع hashed، هشهای وردپرس رو بیصدا دوباره هش میکنه؛ با query builder وارد کن.
- وردپرس 6.8 به بعد پسورد رو اول با HMAC-SHA384 و base64 تبدیل میکنه و بعد bcrypt، پس حذف پیشوند $wp کافی نیست.
- PHP 8.4 هزینهٔ پیشفرض bcrypt رو از ۱۰ به ۱۲ رسوند.
- از Laravel 11، rehash خودکار بعد از لاگین موفق با attempt() انجام میشه.
- بعد از rehash، فاصلههای اضافهٔ دور پسورد دیگه مثل وردپرس نادیده گرفته نمیشن.




