fix: 修复登录时因租户不一致导致的422策略违规错误

在连接建立时记录服务器实际使用的租户代码,而非直接从存储中读取,避免登录时的租户类型判断与会话租户不一致导致的TENANT_CODE_FORBIDDEN错误。新增登录失败后的自修复重试逻辑,当收到422错误且原因是租户代码被禁止时,临时覆盖登录类型并重试一次,同时重置相关状态避免影响后续连接。
This commit is contained in:
Austin
2026-09-22 20:17:39 +08:00
parent 370bf9d5b0
commit 0c38adba78
+43 -4
View File
@@ -150,6 +150,16 @@ export function clearKickedInfo() {
* logins (restoreSession/reconnect re-auth) are untouched -- validateLoginTenant only requires
* login_type when scheme is "basic".
*/
/** The tenant the server session actually resolves to, captured when {hi} is sent (the server
* falls back to 'default' when {hi}.tenant is absent). login_type is decided against THIS value,
* not a fresh store read: the two decisions happen at different moments (hi on connect, login on
* submit), and a store change in between would make login_type disagree with the session tenant,
* which the server rejects with 422 policy violation / TENANT_CODE_FORBIDDEN -- rendered by
* clients as "当前操作不符合安全策略要求". */
let sessionTenantCode: string | null = null;
/** One-shot override used by loginBasic's FORBIDDEN self-heal retry (see there). */
let forcedLoginType: 0 | 1 | null = null;
function installOutgoingPacketPatches(t: Tinode) {
const conn = t._connection;
if (!conn || conn.__tenantPatched) return;
@@ -160,10 +170,13 @@ function installOutgoingPacketPatches(t: Tinode) {
const parsed = JSON.parse(msg);
if (parsed.hi) {
const tenant = useSessionStore.getState().tenant;
// Whichever tenant (or absence of one) this {hi} declares, the server fixes the session
// tenant from it right now -- so remember exactly that, for login_type decisions later.
sessionTenantCode = (tenant?.code || 'default').trim().toLowerCase();
if (tenant?.code) {
parsed.hi.tenant = tenant.code;
msg = JSON.stringify(parsed);
}
msg = JSON.stringify(parsed);
}
} catch {
// Not valid JSON -- send as is.
@@ -172,8 +185,12 @@ function installOutgoingPacketPatches(t: Tinode) {
try {
const parsed = JSON.parse(msg);
if (parsed.login && parsed.login.scheme === 'basic') {
const tenantCode = useSessionStore.getState().tenant?.code || 'default';
parsed.login.login_type = tenantCode.trim().toLowerCase() === 'default' ? 1 : 0;
if (forcedLoginType !== null) {
parsed.login.login_type = forcedLoginType;
} else {
const tenantCode = sessionTenantCode ?? (useSessionStore.getState().tenant?.code || 'default');
parsed.login.login_type = tenantCode.trim().toLowerCase() === 'default' ? 1 : 0;
}
msg = JSON.stringify(parsed);
}
} catch {
@@ -292,6 +309,11 @@ export function resetTinode() {
connectPromise = null;
restorePromise = null;
wasAuthenticated = false;
// Both belong to the torn-down connection's session: the next getTinode() re-derives them from
// the freshly-sent {hi}, and a stale non-null sessionTenantCode here would misclassify the very
// first login_type of the rebuilt connection (which sends hi before any store read matters).
sessionTenantCode = null;
forcedLoginType = null;
setStatus('disconnected');
}
@@ -354,7 +376,24 @@ export function loginBasic(login: string, password: string): Promise<LoginCtrl>
loginPromise = (async () => {
const t = getTinode();
if (!t.isConnected()) await connect();
return t.loginBasic(login, password);
const ctrl = await t.loginBasic(login, password);
// Self-heal the exact failure mode observed live (server validateLoginTenant): an enterprise-
// meaning login_type=0 against a session whose tenant is 'default' is rejected with 422 policy
// violation, reason=TENANT_CODE_FORBIDDEN -- users see "当前操作不符合安全策略要求". That can
// only mean the session tenant is 'default', so retry once with the personal meaning (1) before
// giving up; a success also re-syncs sessionTenantCode so later logins on this connection don't
// repeat the mistake.
if (ctrl.code === 422 && (ctrl.params as { reason?: string } | undefined)?.reason === 'TENANT_CODE_FORBIDDEN') {
forcedLoginType = 1;
try {
const retry = await t.loginBasic(login, password);
if (retry.code < 300) sessionTenantCode = 'default';
return retry;
} finally {
forcedLoginType = null;
}
}
return ctrl;
})().finally(() => {
loginPromise = null;
});