WordPress与CodeIgniter 4用户密码加密同步问题求助
在CodeIgniter 4中验证WordPress用户密码的解决方案
一、先搞懂WordPress的密码机制
- WordPress 3.7+ 已默认使用PHP原生的
password_hash()生成密码哈希,每次生成的哈希值不同是因为自动添加了随机盐,这是安全设计,完全正常。 - WordPress 提供
wp_check_password($password, $hash)函数统一处理密码验证,它会自动兼容旧版的PHPass哈希(如$P$B开头)和新版的PHP原生哈希,无需关心具体格式。 - 你之前用
wp_hash_password()生成的哈希和数据库不一致是正常的——因为盐随机,但只要密码正确,wp_check_password()就能验证通过。
二、在CI4中实现WordPress密码验证
不需要同步哈希格式,直接在CI4的登录流程中复用WordPress的验证逻辑即可,推荐两种方案:
方案1:提取WordPress核心验证代码(无WP依赖)
加载整个WordPress到CI4可能引发冲突,直接提取关键函数到CI4中:
- 创建CI4辅助文件并复制核心代码
创建app/Helpers/wp_password_helper.php,粘贴以下代码(来自WP的wp-includes/pluggable.php及class-phpass.php):
<?php // 从WordPress的wp-config.php中复制以下常量值 define('AUTH_KEY', '你的WP AUTH_KEY'); define('SECURE_AUTH_KEY', '你的WP SECURE_AUTH_KEY'); define('LOGGED_IN_KEY', '你的WP LOGGED_IN_KEY'); define('NONCE_KEY', '你的WP NONCE_KEY'); define('AUTH_SALT', '你的WP AUTH_SALT'); define('SECURE_AUTH_SALT', '你的WP SECURE_AUTH_SALT'); define('LOGGED_IN_SALT', '你的WP LOGGED_IN_SALT'); define('NONCE_SALT', '你的WP NONCE_SALT'); // 复制WordPress的PasswordHash类 class PasswordHash { private $itoa64; private $iteration_count_log2; private $portable_hashes; private $random_state; function __construct($iteration_count_log2, $portable_hashes) { $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) $iteration_count_log2 = 8; $this->iteration_count_log2 = $iteration_count_log2; $this->portable_hashes = $portable_hashes; $this->random_state = microtime() . uniqid('', true); } private function get_random_bytes($count) { $output = ''; if (is_readable('/dev/urandom') && ($fh = @fopen('/dev/urandom', 'rb'))) { $output = fread($fh, $count); fclose($fh); } if (strlen($output) < $count) { $output = ''; for ($i = 0; $i < $count; $i += 16) { $this->random_state = md5(microtime() . $this->random_state); $output .= pack('H*', md5($this->random_state)); } $output = substr($output, 0, $count); } return $output; } private function encode64($input, $count) { $output = ''; $i = 0; do { $value = ord($input[$i++]); $output .= $this->itoa64[$value & 0x3f]; if ($i < $count) $value |= ord($input[$i]) << 8; $output .= $this->itoa64[($value >> 6) & 0x3f]; if ($i++ >= $count) break; if ($i < $count) $value |= ord($input[$i]) << 16; $output .= $this->itoa64[($value >> 12) & 0x3f]; if ($i++ >= $count) break; $output .= $this->itoa64[($value >> 18) & 0x3f]; } while ($i < $count); return $output; } private function gensalt_private($input) { $output = '$P$'; $output .= $this->itoa64[min($this->iteration_count_log2 + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; $output .= $this->encode64($input, 6); return $output; } private function crypt_private($password, $setting) { $output = '*0'; if (substr($setting, 0, 2) == $output) $output = '*1'; $id = substr($setting, 0, 3); # We use "$P$", phpBB3 uses "$H$" for the same thing if ($id != '$P$' && $id != '$H$') return $output; $count_log2 = strpos($this->itoa64, $setting[3]); if ($count_log2 < 7 || $count_log2 > 30) return $output; $count = 1 << $count_log2; $salt = substr($setting, 4, 8); if (strlen($salt) != 8) return $output; # We're kind of forced to use MD5 here since it's the only # cryptographic primitive available in all versions of PHP # currently in use. To implement our own low-level crypto # in PHP would result in much worse performance and # consequently in lower iteration counts and hashes that are # quicker to crack (by non-PHP code). if (PHP_VERSION >= '5') { $hash = md5($salt . $password, true); do { $hash = md5($hash . $password, true); } while (--$count); } else { $hash = pack('H*', md5($salt . $password)); do { $hash = pack('H*', md5($hash . $password)); } while (--$count); } $output = substr($setting, 0, 12); $output .= $this->encode64($hash, 16); return $output; } public function HashPassword($password) { $random = ''; if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) { $random = $this->get_random_bytes(16); $hash = crypt($password, $this->gensalt_blowfish($random)); if (strlen($hash) == 60) return $hash; } if (strlen($random) < 6) $random = $this->get_random_bytes(6); $hash = $this->crypt_private($password, $this->gensalt_private($random)); if (strlen($hash) == 34) return $hash; # Returning '*' on error is safe here, but would _not_ be safe # in a crypt(3)-like function used _both_ for generating new # hashes and for validating passwords against existing hashes. return '*'; } private function gensalt_blowfish($input) { # This one needs to use a different order of characters and a # different encoding scheme from the one in encode64() above. # We care because the last character in our encoded string will # only represent 2 bits. While two known implementations of # bcrypt will happily accept and correct a salt string which # has the 4 unused bits set to non-zero, we do not want to take # chances and we also do not want to waste an additional byte # of entropy. $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $output = '$2a$'; $output .= chr(ord('0') + $this->iteration_count_log2 / 10); $output .= chr(ord('0') + $this->iteration_count_log2 % 10); $output .= '$'; $i = 0; do { $c1 = ord($input[$i++]); $output .= $itoa64[$c1 >> 2]; $c1 = ($c1 & 0x03) << 4; if ($i >= 16) { $output .= $itoa64[$c1]; break; } $c2 = ord($input[$i++]); $c1 |= $c2 >> 4; $output .= $itoa64[$c1]; $c1 = ($c2 & 0x0f) << 2; $c2 = ord($input[$i++]); $c1 |= $c2 >> 6; $output .= $itoa64[$c1]; $output .= $itoa64[$c2 & 0x3f]; } while (1); return $output; } public function CheckPassword($password, $stored_hash) { $hash = $this->crypt_private($password, $stored_hash); if ($hash[0] == '*') $hash = crypt($password, $stored_hash); return $hash == $stored_hash; } } // WordPress密码验证核心函数 function wp_check_password($password, $hash, $user_id = '') { static $wp_hasher; if (empty($wp_hasher)) { $wp_hasher = new PasswordHash(8, true); } $check = $wp_hasher->CheckPassword($password, $hash); // 兼容PHP原生哈希格式 if (!$check && (str_starts_with($hash, '$2y$') || str_starts_with($hash, '$2a$'))) { $check = password_verify($password, $hash); } return $check; }
- 在CI4登录控制器中调用验证
<?php namespace App\Controllers; use CodeIgniter\Controller; class Login extends Controller { public function index() { helper('wp_password'); $inputPassword = $this->request->getPost('password'); $username = $this->request->getPost('username'); // 从wp_users表查询用户哈希 $user = $this->db->table('wp_users') ->where('user_login', $username) ->get() ->getRow(); if ($user && wp_check_password($inputPassword, $user->user_pass)) { // 验证通过,执行CI4登录逻辑 session()->set('user_id', $user->ID); return redirect()->to('/dashboard'); } else { return redirect()->back()->with('error', '账号或密码错误'); } } }
方案2:轻量加载WordPress核心(同服务器场景)
如果CI4和WordPress在同一服务器,可直接加载WP核心调用验证:
<?php namespace App\Controllers; use CodeIgniter\Controller; class Login extends Controller { public function index() { // 加载WordPress核心文件 require_once '/path/to/your/wordpress/wp-load.php'; $inputPassword = $this->request->getPost('password'); $username = $this->request->getPost('username'); $user = $this->db->table('wp_users') ->where('user_login', $username) ->get() ->getRow(); if ($user && wp_check_password($inputPassword, $user->user_pass)) { // 执行CI4登录逻辑 session()->set('user_id', $user->ID); return redirect()->to('/dashboard'); } else { return redirect()->back()->with('error', '账号或密码错误'); } } }
注意:此方式可能引发全局变量冲突,需测试兼容性。
三、优化建议
- 验证通过后,可将用户密码用CI4的
password_hash()重新加密并更新到应用用户表,后续登录直接用CI4的password_verify()验证,减少对WP的依赖。 - 用户同步仅需同步账号、邮箱等基础信息,无需同步哈希,登录时实时验证WP哈希即可。
内容的提问来源于stack exchange,提问作者mcnord
相关产品推荐
相关产品推荐

