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:
+64
-5
@@ -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
@@ -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("认证服务不可用,且本地账号存储未加载");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user