第48批(2/2):网页改密强度对齐游戏+实时校验+成功面板留屏;容错解盐修脏盐玩家网页端恒密码错误;bing壁纸代理地址补api前缀
- core.php: 新增 pwdRuleError(6-20位+大写+小写+数字,与游戏 PasswordUtils.validate 同一把尺子) 与 saltDecode(镜像Java清洗,strict解码前先剔除非base64字符) - account.php: acctChangePassword 套用 pwdRuleError;acctPwdCheck 改走 saltDecode - sync.php: changePassword/resetPassword 套用 pwdRuleError;changePassword 老密码校验改走 saltDecode - player.php: 改密弹窗加实时4项校验清单(oninput更新✓/✗,全绿才可提交); 成功后不关弹窗,展示明文新密码+复制按钮+同步说明(供截屏保存),保留 notifyJavaSync - wallpaper.php: bing/NASA 代理地址补 api/ 前缀(原相对路径经前端路由转发后404); 缓存键 bing->bing_v2(旧6h缓存存着坏URL,版本化立即生效)
This commit is contained in:
+4
-2
@@ -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('账号未设置密码');
|
||||
|
||||
+9
-6
@@ -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('密码盐值无效');
|
||||
}
|
||||
|
||||
+7
-4
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+49
-6
@@ -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(`
|
||||
<div style="font-size:13px;color:var(--dim);margin-bottom:10px">两层验证已通过,请设置新密码</div>
|
||||
<div class="row"><label>新密码</label><input type="password" id="acctNewPwd" placeholder="6-20位" autocomplete="new-password" style="width:100%;box-sizing:border-box;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px"></div>
|
||||
<div style="font-size:13px;color:var(--dim);margin-bottom:10px">两层验证已通过,请设置新密码(与游戏内同一套规则)</div>
|
||||
<div class="row"><label>新密码</label><input type="password" id="acctNewPwd" placeholder="6-20位" autocomplete="new-password" oninput="acctPwdChecklist()" style="width:100%;box-sizing:border-box;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px"></div>
|
||||
<div id="acctPwdRules" style="display:grid;grid-template-columns:1fr 1fr;gap:4px 10px;margin:8px 0 4px;font-size:12px">
|
||||
<div id="pwrLen">○ 长度 6-20 位</div>
|
||||
<div id="pwrUp">○ 含大写字母</div>
|
||||
<div id="pwrLow">○ 含小写字母</div>
|
||||
<div id="pwrNum">○ 含数字</div>
|
||||
</div>
|
||||
<div class="row"><label>确认新密码</label><input type="password" id="acctNewPwd2" placeholder="再输入一次" autocomplete="new-password" style="width:100%;box-sizing:border-box;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px" onkeydown="if(event.key==='Enter')document.getElementById('modalConfirm').click()"></div>
|
||||
<div id="acctErr" style="color:var(--red);font-size:12px;min-height:16px"></div>`,
|
||||
'确认修改', () => acctDoChangePassword(ticket));
|
||||
acctPwdChecklist();
|
||||
setTimeout(() => { const p = document.getElementById('acctNewPwd'); if (p) p.focus(); }, 120);
|
||||
}
|
||||
|
||||
// ---- ★第48批#245:密码强度实时校验清单(与游戏 PasswordUtils.validate 同一把尺子) ----
|
||||
function acctPwdChecklist() {
|
||||
const v = (document.getElementById('acctNewPwd') || {}).value || '';
|
||||
const checks = [
|
||||
['pwrLen', v.length >= 6 && v.length <= 20],
|
||||
['pwrUp', /[A-Z]/.test(v)],
|
||||
['pwrLow', /[a-z]/.test(v)],
|
||||
['pwrNum', /[0-9]/.test(v)]
|
||||
];
|
||||
let allOk = v.length > 0;
|
||||
for (const [id, ok] of checks) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.textContent = (ok ? '✓ ' : '✗ ') + el.textContent.slice(2);
|
||||
el.style.color = ok ? 'var(--green, #4caf50)' : 'var(--dim)';
|
||||
}
|
||||
if (!ok) allOk = false;
|
||||
}
|
||||
const btn = document.getElementById('modalConfirm');
|
||||
if (btn && !btn.disabled) btn.disabled = !allOk; // 全绿才允许提交
|
||||
return allOk;
|
||||
}
|
||||
|
||||
async function acctDoChangePassword(ticket) {
|
||||
const p1 = (document.getElementById('acctNewPwd') || {}).value || '';
|
||||
const p2 = (document.getElementById('acctNewPwd2') || {}).value || '';
|
||||
if (!p1 || !p2) { acctErr('请填写两遍新密码'); return; }
|
||||
if (p1 !== p2) { acctErr('两次输入的密码不一致'); return; }
|
||||
if (p1.length < 6 || p1.length > 20) { acctErr('密码长度必须为6-20位'); return; }
|
||||
if (!acctPwdChecklist()) { acctErr('密码需满足全部4项要求(6-20位 + 大写 + 小写 + 数字)'); return; }
|
||||
const btn = document.getElementById('modalConfirm');
|
||||
btn.disabled = true; acctErr('提交中...');
|
||||
try {
|
||||
const res = await apiPost('account.php', {action: 'change_password', ticket: ticket, new_password: p1, confirm_password: p2});
|
||||
btn.disabled = false;
|
||||
if (!res.success) { acctErr(res.message || '修改失败'); return; }
|
||||
closeModal();
|
||||
showToast('✅ 密码修改成功', 'success');
|
||||
if (!res.success) { acctErr(res.message || '修改失败'); acctPwdChecklist(); return; }
|
||||
try { notifyJavaSync(); } catch (e) {}
|
||||
acctShowPwdSuccess(p1);
|
||||
acctRefreshStatus();
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
acctErr('连接失败: ' + e.message);
|
||||
acctPwdChecklist();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ★第48批#246:改密成功面板(不关弹窗,明文密码留屏供截屏保存) ----
|
||||
function acctShowPwdSuccess(newPwd) {
|
||||
_lastChangedPwd = newPwd;
|
||||
acctModalOpen('✅ 密码修改成功');
|
||||
acctModalSet(`
|
||||
<div style="font-size:13px;color:var(--dim);margin-bottom:10px">请立即截屏或复制保存以下新密码</div>
|
||||
<div style="font-family:monospace;font-size:16px;letter-spacing:1px;padding:10px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;text-align:center;word-break:break-all;user-select:all">${escapeHtml(newPwd)}</div>
|
||||
<button class="btn" style="width:100%;font-size:13px;margin-top:8px" onclick="acctCopy(_lastChangedPwd)">📋 复制密码</button>
|
||||
<div style="color:var(--dim);font-size:12px;margin-top:12px;line-height:1.6">已请求同步到游戏端(通常 1-2 分钟内生效)。<br>若游戏内仍提示密码错误,请退出重进后再试一次。</div>`,
|
||||
'我已保存,关闭', () => { closeModal(); acctRefreshStatus(); });
|
||||
}
|
||||
|
||||
// ---- 绑定二次验证器:生成密钥 → 扫码 → 动态码确认 → 一次性返回恢复代码 ----
|
||||
async function acctFlowTotp(ticket) {
|
||||
acctModalOpen('🛡️ 绑定二次验证器');
|
||||
|
||||
Reference in New Issue
Block a user