From 188e0837193473a0df817a9e8a47d5b235bcfe4d Mon Sep 17 00:00:00 2001 From: canglan Date: Fri, 21 Aug 2026 21:01:30 +0800 Subject: [PATCH] refactor: auth expiry - sync JWT exp check, remove async validation Previous async validateAuth() approach caused: recursion loop, wrong /me path (404 -> session cleared), init() fetching auth-required settings without token (cleared session every load). New synchronous approach: - Auth.expired(): decodes JWT payload locally, checks exp timestamp - Auth.logged(): token+user exist AND not expired; auto-clears stale session on expiry (no network, no async, no loops) - route(): simple sync guard - if !Auth.logged() reset + redirect to #/login; home page stays public (clears stale cookie only) - API.req: 401 response -> Auth.reset() + redirect to login unless on public page (fallback for server-side revocation/invalid signature) - Removed validateAuth/_validating/_doRoute entirely --- public/js/api.js | 6 ++++++ public/js/app.js | 44 ++------------------------------------------ public/js/auth.js | 20 +++++++++++++++++++- 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/public/js/api.js b/public/js/api.js index b7b03ae..cbfec53 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -34,6 +34,12 @@ const API = { const ct = res.headers.get('content-type'); if (ct && ct.includes('application/json')) { const json = await res.json(); + if (res.status === 401) { + // 服务端判定未登录/过期: 清除本地登录态, 非公开页跳转登录 + Auth.reset(); + const page = (location.hash || '#/home').replace('#/', '').split('/')[0]; + if (!['home', 'login', 'register', 'forgot', 'reset', 'verify', 'install'].includes(page)) location.hash = '#/login'; + } if (!res.ok) throw new Error(json.error || '请求失败'); return json; } diff --git a/public/js/app.js b/public/js/app.js index b4ccad4..53355cf 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -86,28 +86,6 @@ const App = { closeModal() { document.getElementById('modal-overlay').classList.add('hidden'); }, navigate(page, param) { location.hash = `#/${page}` + (param ? `/${param}` : ''); }, - _validating: false, - - // 验证 token 有效性(轻量级,失败则清除登录态) - async validateAuth() { - if (this._validating) return null; // 防止递归 - this._validating = true; - if (!Auth.logged()) { this._validating = false; return false; } - try { - await API.get('/auth/me'); - this._validating = false; - return true; - } catch (e) { - this._validating = false; - if (e && e.message && e.message.includes('登录')) { - Auth.reset(); - return false; - } - // 非登录错误(网络/404/500)不视为登录失效,放行路由 - return true; - } - }, - route() { const hash = location.hash || '#/home'; const parts = hash.replace('#/', '').split('/'); @@ -123,31 +101,13 @@ const App = { if (page === 'home') { this.showPublic('home', param); return; } if (page === 'login' || page === 'register' || page === 'forgot' || page === 'reset') { this.showPublic(page); return; } - // 访问受保护页面前先验证 token 有效性 - if (Auth.logged() && !this._validating) { - this._validating = true; // 标记正在验证 - this.validateAuth().then(valid => { - this._validating = false; - if (!valid) { - location.hash = '#/login'; - return; - } - this._doRoute(page, param); - }).catch(() => { - this._validating = false; - Auth.reset(); - location.hash = '#/login'; - }); - return; - } + // 受保护页面: Auth.logged() 内部解码 JWT exp,过期自动清除登录态(同步,无异步循环) if (!Auth.logged()) { + Auth.reset(); location.hash = '#/login'; return; } - // 正在验证中,不做任何操作(等待验证完成) - }, - _doRoute(page, param) { this.showLayout(); const sidebarPage = (page === 'tickets' && param.startsWith('create')) ? 'tickets-create' : page; this.renderSidebar(sidebarPage); diff --git a/public/js/auth.js b/public/js/auth.js index 87fa499..bcecf19 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -23,7 +23,25 @@ const Auth = { token() { return localStorage.getItem(this.TK); }, user() { try { return JSON.parse(localStorage.getItem(this.US)); } catch { return null; } }, - logged() { return !!(this.token() && this.user()); }, + + // JWT payload 里的 exp(秒) 已过期? + expired() { + const t = this.token(); + if (!t) return true; + try { + let b64 = t.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); + b64 = b64.padEnd(b64.length + (4 - b64.length % 4) % 4, '='); + const payload = JSON.parse(atob(b64)); + return payload.exp ? payload.exp * 1000 < Date.now() : false; + } catch { return true; } + }, + + // 登录态判断: token+user 存在 且 未过期;过期自动清除(不跳转) + logged() { + if (!this.token() || !this.user()) return false; + if (this.expired()) { this.reset(); return false; } + return true; + }, async login(username, password) { const d = await API.post('/auth/login', { username, password });