diff --git a/api/account.php b/api/account.php index 9c2f722..006219d 100644 --- a/api/account.php +++ b/api/account.php @@ -314,7 +314,7 @@ function acctCredentials($player) { /** 第一层密码比对:只认主密码("完整密码"语义,临时密码不算)。 */ function acctPwdCheck($cred, $pwd) { if (!$cred || !isset($cred['password_hash']) || $cred['password_hash'] === '') return false; - $saltBytes = base64_decode((string)$cred['salt'], true); + $saltBytes = saltDecode((string)$cred['salt']); // ★第48批:容错解盐(core.php saltDecode) if ($saltBytes === false || $saltBytes === '') return false; $calc = base64_encode(hash('sha256', $saltBytes . $pwd, true)); return hash_equals((string)$cred['password_hash'], $calc); @@ -693,7 +693,9 @@ function acctChangePassword() { $confirm = (string)getParam('confirm_password', ''); if ($newPwd === '') error('请输入新密码'); if ($confirm !== '' && $confirm !== $newPwd) error('两次输入的新密码不一致'); - if (strlen($newPwd) < 6 || strlen($newPwd) > 20) error('密码长度必须为6-20位'); + // ★第48批#245:与游戏 PasswordUtils.validate 同一把尺子(6-20位+大写+小写+数字) + $ruleErr = pwdRuleError($newPwd); + if ($ruleErr !== null) error($ruleErr); $cred = acctCredentials($player); if (!$cred) error('账号未设置密码'); diff --git a/api/sync.php b/api/sync.php index a5e20ba..481b820 100644 --- a/api/sync.php +++ b/api/sync.php @@ -4683,7 +4683,9 @@ function resetPassword() { if (!$resetToken) error('缺少reset_token参数'); if (!$newPassword) error('缺少new_password参数'); - if (strlen($newPassword) < 6) error('密码长度至少6位'); + // ★第48批#245:与游戏 PasswordUtils.validate 同一把尺子(6-20位+大写+小写+数字) + $ruleErr = pwdRuleError($newPassword); + if ($ruleErr !== null) error($ruleErr); $db = getDB(); @@ -4839,9 +4841,10 @@ function changePassword() { error('缺少必要参数'); } - // 验证新密码格式 - if (strlen($newPwd) < 6 || strlen($newPwd) > 20) { - error('密码长度必须为6-20位'); + // ★第48批#245:与游戏 PasswordUtils.validate 同一把尺子(6-20位+大写+小写+数字) + $ruleErr = pwdRuleError($newPwd); + if ($ruleErr !== null) { + error($ruleErr); } $db = getDB(); @@ -4854,8 +4857,8 @@ function changePassword() { error('玩家不存在'); } - // 验证旧密码 - $saltBytes = base64_decode($row['salt'], true); + // 验证旧密码(★第48批#244:容错解盐,脏盐玩家也能在网页改密) + $saltBytes = saltDecode($row['salt']); if ($saltBytes === false || $saltBytes === '') { error('密码盐值无效'); } diff --git a/api/wallpaper.php b/api/wallpaper.php index 4ebc314..892d76a 100644 --- a/api/wallpaper.php +++ b/api/wallpaper.php @@ -80,7 +80,8 @@ $act = isset($_GET['action']) ? $_GET['action'] : ''; /* ===== 每日壁纸元数据(第47批:多源 fallback 链) ===== */ if ($act === 'bing') { - $cached = wpjCacheGet('bing', WPJ_BING_TTL); + // ★第48批#247:key 版本化 bing -> bing_v2(旧缓存存着无 api/ 前缀的坏 URL,须失效) + $cached = wpjCacheGet('bing_v2', WPJ_BING_TTL); if ($cached !== null) { wpjOut(true, 'ok', $cached); } $data = null; @@ -94,7 +95,8 @@ if ($act === 'bing') { $direct = $base . $img['urlbase'] . '_1920x1080.jpg'; $data = array( // 前端优先用代理地址(防防盗链),direct 作兜底直连 - 'url' => 'wallpaper.php?action=image&u=' . rawurlencode($direct), + // ★第48批#247:补 api/ 前缀(原相对路径经前端路由转发后 404,图片取不到) + 'url' => 'api/wallpaper.php?action=image&u=' . rawurlencode($direct), 'direct' => $direct, 'source' => 'bing', 'title' => isset($img['title']) ? $img['title'] : '', @@ -111,7 +113,8 @@ if ($act === 'bing') { if (is_array($j) && !empty($j['hdurl']) && preg_match('#^https?://#i', $j['hdurl'])) { $direct = $j['hdurl']; $data = array( - 'url' => 'wallpaper.php?action=image&u=' . rawurlencode($direct), + // ★第48批#247:补 api/ 前缀(同 bing) + 'url' => 'api/wallpaper.php?action=image&u=' . rawurlencode($direct), 'direct' => $direct, 'source' => 'nasa', 'title' => isset($j['title']) ? $j['title'] : 'NASA 每日天文图', @@ -134,7 +137,7 @@ if ($act === 'bing') { ); } - wpjCacheSet('bing', $data); + wpjCacheSet('bing_v2', $data); wpjOut(true, 'ok', $data); } diff --git a/core.php b/core.php index d20706c..edbae35 100644 --- a/core.php +++ b/core.php @@ -3040,6 +3040,39 @@ function storeTempPassword($playerName, $tempHash, $expire) { } } +/** + * 游戏同款密码强度规则(第48批#245,与 Java PasswordUtils.validate 对齐): + * 长度 6-20 位 + 至少 1 个大写字母 + 至少 1 个小写字母 + 至少 1 个数字。 + * 网页端改密/重置必须走同一把尺子,避免出现「网页能设、游戏登不上」的密码。 + * + * @return string|null 通过返回 null;不通过返回错误文案(可直接 error()) + */ +function pwdRuleError($pwd) { + $pwd = (string)$pwd; + $len = strlen($pwd); + if ($len < 6 || $len > 20) return '密码长度必须为6-20位'; + if (!preg_match('/[A-Z]/', $pwd)) return '密码需包含大写字母'; + if (!preg_match('/[a-z]/', $pwd)) return '密码需包含小写字母'; + if (!preg_match('/[0-9]/', $pwd)) return '密码需包含数字'; + return null; +} + +/** + * 容错解盐(第48批):镜像 Java PasswordUtils 的清洗逻辑。 + * 游戏侧偶发脏盐(含 JSON 转义反斜杠等非 base64 字符), + * Java 端 hash 前会 replaceAll("[^A-Za-z0-9+/=]") 清洗后再用,游戏端正常; + * PHP strict base64_decode 却直接失败 → 该玩家网页端永远密码错误。 + * 故 PHP 侧同款清洗后再 strict 解码——清洗等价于 Java,哈希结果不变。 + * @return string|false 解出的原始盐字节;清洗后仍无法解码返回 false + */ +function saltDecode($salt) { + $clean = preg_replace('/[^A-Za-z0-9+\/=]/', '', (string)$salt); + if ($clean === '' || $clean === null) return false; + $bytes = base64_decode($clean, true); + if ($bytes === false || $bytes === '') return false; + return $bytes; +} + /** * 验证Web登录密码(优先校验临时密码,再校验主密码) * 返回: 'temp' 表示临时密码, 'main' 表示主密码, false 表示失败 @@ -3067,7 +3100,7 @@ function verifyWebLoginPassword($playerName, $password) { $tempExpire = (int)$row['temp_pw_expire']; if (time() < $tempExpire && $row['temp_password_hash'] !== '') { $salt = $row['salt']; - $saltBytes = base64_decode($salt, true); + $saltBytes = saltDecode($salt); // ★第48批:容错解盐(镜像Java清洗) if ($saltBytes !== false && $saltBytes !== '') { $rawHash = hash('sha256', $saltBytes . $password, true); $computedHash = base64_encode($rawHash); @@ -3081,7 +3114,7 @@ function verifyWebLoginPassword($playerName, $password) { // 2. 再检查主密码 $storedHash = $row['password_hash']; $salt = $row['salt']; - $saltBytes = base64_decode($salt, true); + $saltBytes = saltDecode($salt); // ★第48批:容错解盐(镜像Java清洗) if ($saltBytes === false || $saltBytes === '') return false; $rawHash = hash('sha256', $saltBytes . $password, true); diff --git a/player.php b/player.php index 4250ef4..95538b3 100644 --- a/player.php +++ b/player.php @@ -3011,6 +3011,7 @@ if ($currentVersion !== $BUILD_VERSION) { let _acctState = null; let _lastRecoveryCodes = []; + let _lastChangedPwd = ''; // ★第48批#246:改密成功面板明文密码(复制按钮用) function startAccountVerify(action, label) { if (IS_PREVIEW || !TOKEN || !currentPlayer) { showToast('请先登录游戏后再操作', 'error'); return; } @@ -3203,36 +3204,78 @@ if ($currentVersion !== $BUILD_VERSION) { function acctPromptNewPassword(ticket) { acctModalOpen('🛡️ 修改登录密码'); acctModalSet(` -