where(['emp_no = :emp_no'], [':emp_no' => $emp_no])->fetch(); } /** * 验证登录(支持 bcrypt 和旧版 MD5 兼容) */ public function validateLogin($emp_no, $password) { // 先按工号查找用户 $user = $this->where(['emp_no = :emp_no'], [':emp_no' => $emp_no])->fetch(); if (!$user) { $ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown'; $sess = session_id() ? hash('sha256', session_id()) : 'none'; error_log("[LOGIN_DIAG] emp_no_len=" . strlen($emp_no) . " emp_no_raw='" . $emp_no . "' found_user=0 ua={$ua} sess_hash={$sess}"); return false; } // 判断密码是否为 bcrypt 格式(bcrypt 哈希总是以 $2y$ 开头) $isBcrypt = (substr($user['password'], 0, 4) === '$2y$'); $pwdFmt = $isBcrypt ? 'bcrypt' : (strlen($user['password']) === 32 ? 'md5' : 'other'); $ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown'; $sess = session_id() ? hash('sha256', session_id()) : 'none'; $pwdLen = strlen($password); error_log("[LOGIN_DIAG] emp_no_len=" . strlen($emp_no) . " emp_no_raw='" . $emp_no . "' found_user=1 pwd_fmt={$pwdFmt} pwd_len={$pwdLen} ua={$ua} sess_hash={$sess}"); if ($isBcrypt) { if (password_verify($password, $user['password'])) { // 如果算法需要升级,自动更新哈希 if (password_needs_rehash($user['password'], PASSWORD_BCRYPT)) { $this->updatePasswordHash($user['id'], $password); } return $user; } error_log("[LOGIN_DIAG] PWD_VERIFY_FAIL pwd_len={$pwdLen} pwd_hash_len=" . strlen($user['password']) . " ua={$ua} sess_hash={$sess}"); return false; } // 兼容旧版 MD5 密码 if (md5($password) === $user['password']) { // 自动升级为 bcrypt $this->updatePasswordHash($user['id'], $password); return $user; } return false; } /** * 更新密码哈希为 bcrypt */ private function updatePasswordHash($id, $password) { $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); $this->where(['id = :id'], [':id' => $id])->update(['password' => $hash]); } /** * 生成密码哈希 */ public function hashPassword($password) { return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); } /** * 密码复杂度校验(P1:强制强密码策略) * 要求:≥12 位,且同时包含大写字母、小写字母、数字、特殊字符 * @throws \RuntimeException 不满足时抛出异常 */ public function validatePasswordStrength($password) { if (!is_string($password) || $password === '') { throw new \RuntimeException('密码不能为空'); } if (mb_strlen($password) < 12) { throw new \RuntimeException('密码长度至少 12 位'); } if (!preg_match('/[A-Z]/', $password)) { throw new \RuntimeException('密码必须包含至少一个大写字母'); } if (!preg_match('/[a-z]/', $password)) { throw new \RuntimeException('密码必须包含至少一个小写字母'); } if (!preg_match('/[0-9]/', $password)) { throw new \RuntimeException('密码必须包含至少一个数字'); } if (!preg_match('/[^A-Za-z0-9]/', $password)) { throw new \RuntimeException('密码必须包含至少一个特殊字符(如 !@#$%^&*)'); } // 拒绝常见弱密码 $weak = ['123456', 'password', 'admin123', 'qwerty', '111111', '12345678', 'abc123']; if (in_array(strtolower($password), $weak, true)) { throw new \RuntimeException('密码过于常见,请更换'); } } // 重置密码 public function resetPassword($id, $new_password) { $this->validatePasswordStrength($new_password); $hash = $this->hashPassword($new_password); return $this->where(['id = :id'], [':id' => $id])->update(['password' => $hash]); } // 获取所有员工 public function getAll() { return $this->order(['id ASC'])->fetchAll(); } // 添加员工 public function addEmployee($data) { if (!isset($data['password']) || empty(trim($data['password']))) { // 生成随机强密码 $randomPwd = bin2hex(random_bytes(8)); // 16位随机密码 $data['password'] = $this->hashPassword($randomPwd); // 返回生成的密码供管理员告知用户 $data['_generated_password'] = $randomPwd; } else { // P1:密码复杂度校验(≥12位,含大小写+数字+特殊字符) $this->validatePasswordStrength($data['password']); $data['password'] = $this->hashPassword($data['password']); } // 移除 _generated_password 再写入 $generatedPwd = $data['_generated_password'] ?? null; unset($data['_generated_password']); $id = $this->add($data); if ($id && $generatedPwd) { // 返回生成的密码 $this->_lastGeneratedPwd = $generatedPwd; } return $id; } // 获取最后生成的密码 public function getLastGeneratedPwd() { return $this->_lastGeneratedPwd ?? null; } private $_lastGeneratedPwd = null; // 更新员工 public function updateEmployee($id, $data) { unset($data['password']); // 不允许直接更新密码 return $this->where(['id = :id'], [':id' => $id])->update($data); } // 根据类目获取员工列表 public function getByCategory($category) { if ($category === 'all') { return $this->order(['id ASC'])->fetchAll(); } return $this->where(['category = :cat OR role = :super'], [ ':cat' => $category, ':super' => 'super_admin' ])->order(['id ASC'])->fetchAll(); } }