fix: 修复 http 纯静态部署下本地账号登录不可用(issue #131)

- user_store.js:crypto.subtle 仅安全上下文可用,http://:8090 下为 undefined
  导致播种/校验静默失败;新增纯 JS SHA-256 降级实现(已对照 Node crypto
  验证一致,含非 ASCII),WebCrypto 可用时仍优先
- session.js:/auth/login 返回 5xx/非 JSON 时判定为后端不可用并降级本地
  账号,仅 4xx 视为真实鉴权失败(原逻辑把 nginx 500 HTML 误判为密码错误)
This commit is contained in:
2026-08-05 16:52:45 +08:00
parent c0af291502
commit e9787ecb22
2 changed files with 77 additions and 16 deletions
+64 -5
View File
@@ -29,12 +29,71 @@ var UserStore = (function () {
crypto.getRandomValues(buf);
return Array.from(buf).map(function (b) { return b.toString(16).padStart(2, "0"); }).join("");
}
/* 纯 JS SHA-256(geraintluff 实现,公有领域):crypto.subtle 仅在
* 安全上下文(https / localhost)可用,纯静态 http 部署(如 :8090)
* 必须降级到此实现,否则本地账号无法播种/校验。 */
function sha256Js(ascii) {
function rr(v, a) { return (v >>> a) | (v << (32 - a)); }
var mp = Math.pow, mw = mp(2, 32), result = "", words = [];
var bitLen = ascii.length * 8;
var hash = (sha256Js.h = sha256Js.h || []);
var k = (sha256Js.k = sha256Js.k || []);
var pc = k.length, comp = {};
for (var c = 2; pc < 64; c++) {
if (!comp[c]) {
for (var i = 0; i < 313; i += c) comp[i] = c;
hash[pc] = (mp(c, 0.5) * mw) | 0;
k[pc++] = (mp(c, 1 / 3) * mw) | 0;
}
}
ascii += "\x80";
while ((ascii.length % 64) - 56) ascii += "\x00";
for (var i = 0; i < ascii.length; i++) {
var j = ascii.charCodeAt(i);
if (j >> 8) throw new Error("sha256Js: 仅接受字节串");
words[i >> 2] |= j << (((3 - i) % 4) * 8);
}
words[words.length] = (bitLen / mw) | 0;
words[words.length] = bitLen;
for (var j = 0; j < words.length;) {
var w = words.slice(j, (j += 16));
var old = hash;
hash = hash.slice(0, 8);
for (var i = 0; i < 64; i++) {
var w15 = w[i - 15], w2 = w[i - 2];
var a = hash[0], e = hash[4];
var t1 = hash[7] + (rr(e, 6) ^ rr(e, 11) ^ rr(e, 25)) +
((e & hash[5]) ^ (~e & hash[6])) + k[i] +
(w[i] = i < 16 ? w[i] : (w[i - 16] +
(rr(w15, 7) ^ rr(w15, 18) ^ (w15 >>> 3)) + w[i - 7] +
(rr(w2, 17) ^ rr(w2, 19) ^ (w2 >>> 10))) | 0);
var t2 = (rr(a, 2) ^ rr(a, 13) ^ rr(a, 22)) +
((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
hash = [(t1 + t2) | 0].concat(hash);
hash[4] = (hash[4] + t1) | 0;
}
for (var i = 0; i < 8; i++) hash[i] = (hash[i] + old[i]) | 0;
}
for (var i = 0; i < 8; i++) {
for (var j = 3; j + 1; j--) {
var b = (hash[i] >> (j * 8)) & 255;
result += (b < 16 ? "0" : "") + b.toString(16);
}
}
return result;
}
function utf8Bytes(str) { return unescape(encodeURIComponent(str)); }
function hashPassword(password, salt) {
var data = new TextEncoder().encode(salt + ":" + password);
return crypto.subtle.digest("SHA-256", data).then(function (buf) {
return Array.from(new Uint8Array(buf))
.map(function (b) { return b.toString(16).padStart(2, "0"); }).join("");
});
var data = salt + ":" + password;
// 优先 WebCrypto(安全上下文);不可用时降级纯 JS SHA-256(http 部署)
if (window.crypto && crypto.subtle && crypto.subtle.digest) {
var encoded = new TextEncoder().encode(data);
return crypto.subtle.digest("SHA-256", encoded).then(function (buf) {
return Array.from(new Uint8Array(buf))
.map(function (b) { return b.toString(16).padStart(2, "0"); }).join("");
}).catch(function () { return sha256Js(utf8Bytes(data)); });
}
return Promise.resolve(sha256Js(utf8Bytes(data)));
}
/* 默认演示账号(Ant Design Pro 风格重构新增):纯静态部署(无 core/auth
+13 -11
View File
@@ -124,18 +124,20 @@ var IAOP_SESSION = (function () {
saveSession(d.user);
return d.user;
});
// 后端在线但拒绝(401 等):是真实鉴权失败,不降级
return r.json().then(function (d) {
throw new Error((d && d.error) || "用户名或密码错误");
});
}).catch(function (e) {
if (e && e.message && e.message !== "timeout" &&
e.message !== "no backend session" &&
e.message !== "Failed to fetch" &&
e.name !== "TypeError" && !/fetch/i.test(e.message || "")) {
throw e; // 后端鉴权失败,直接抛
// 4xx = 后端在线且拒绝(真实鉴权失败);5xx/网关错误 = 后端不可用,降级本地
if (r.status >= 400 && r.status < 500) {
return r.text().then(function (t) {
var msg = "用户名或密码错误";
try { msg = (JSON.parse(t) || {}).error || msg; } catch (e) {}
var err = new Error(msg);
err.authReject = true;
throw err;
});
}
// 后端不可达 → 本地账号校验
throw new Error("backend-http-" + r.status);
}).catch(function (e) {
if (e && e.authReject) throw e; // 后端鉴权失败,不降级
// 超时 / 网络错误 / 5xx / 响应非 JSON 等一切后端不可用情形 → 本地账号校验
if (typeof UserStore === "undefined") {
throw new Error("认证服务不可用,且本地账号存储未加载");
}