feat(static): add frontend token auto-refresh for dnfs page
- New token_manager.js: silent refresh timer (80% lifetime) + global fetch 401 interceptor with one-shot retry. - dnfs.py /whoami now returns iat, created_at, keep_time for expiry estimation. - Dnfs.html loads token_manager.js before dnfs.js (zero change to dnfs.js). - Graceful fallback: stale tokens without new fields, refresh failures, and /refresh_token 401 itself all degrade to the existing 401→login flow.
This commit is contained in:
1 parent
254fbf4628
commit
6649b31e17
3 files changed
+129
-2
No files matched your search
+10
-2
@@ -288,11 +288,19 @@ def whoami():
|
|||||||
if info is None:
|
if info is None:
|
||||||
return jsonify({'login': False}), 200
|
return jsonify({'login': False}), 200
|
||||||
|
|
||||||
return jsonify({
|
resp = {
|
||||||
'login': True,
|
'login': True,
|
||||||
'user': info.get('user'),
|
'user': info.get('user'),
|
||||||
'is_admin': bool(info.get('is_admin'))
|
'is_admin': bool(info.get('is_admin'))
|
||||||
}), 200
|
}
|
||||||
|
# 附加 Token 生命周期信息,供前端 Token 管理器估算过期时间
|
||||||
|
if info.get('iat'):
|
||||||
|
resp['iat'] = info['iat']
|
||||||
|
if info.get('created_at'):
|
||||||
|
resp['created_at'] = info['created_at']
|
||||||
|
if info.get('keep_time'):
|
||||||
|
resp['keep_time'] = info['keep_time']
|
||||||
|
return jsonify(resp), 200
|
||||||
|
|
||||||
|
|
||||||
# eg: {"id": 1}
|
# eg: {"id": 1}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.0/markdown-it.min.js"></script>-->
|
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.0/markdown-it.min.js"></script>-->
|
||||||
<script src="js/markdown-it-12_0_0.js"></script>
|
<script src="js/markdown-it-12_0_0.js"></script>
|
||||||
<script src="js/validation.js"></script>
|
<script src="js/validation.js"></script>
|
||||||
|
<script src="js/token_manager.js"></script>
|
||||||
<script src="js/dnfs.js"></script>
|
<script src="js/dnfs.js"></script>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Token 生命周期管理器
|
||||||
|
* - 定时静默刷新:在 Token 80% 生命周期时提前调 /refresh_token
|
||||||
|
* - 全局 fetch 拦截器:收到 401 先尝试刷新一次,成功则重试原请求,失败则原样返回 401
|
||||||
|
* - 依赖:gateway POST /refresh_token(Cookie q4_token 自动携带)
|
||||||
|
* - 无侵入:与 dnfs.js 完全解耦
|
||||||
|
*/
|
||||||
|
|
||||||
|
let _tokenExpireAt = null;
|
||||||
|
let _refreshTimer = null;
|
||||||
|
|
||||||
|
const REFRESH_ADVANCE = 0.8; // 在 80% 生命周期时提前刷新
|
||||||
|
const REFRESH_MIN_GAP = 60; // 两次刷新最少间隔(秒)
|
||||||
|
const RETRY_COOLDOWN = 15; // 刷新失败后冷却期(秒)
|
||||||
|
|
||||||
|
/* ---------- 过期时间估算:通过 /whoami 获取 iat + keep_time ---------- */
|
||||||
|
|
||||||
|
async function estimateExpiry() {
|
||||||
|
try {
|
||||||
|
const res = await _rawFetch('/whoami', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: '{}'
|
||||||
|
});
|
||||||
|
if (!res.ok) return false;
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.login) return false;
|
||||||
|
|
||||||
|
// 优先 iat(首次签发时间,刷新不变),回退 created_at(兼容旧 Token)
|
||||||
|
const issueStr = data.iat || data.created_at;
|
||||||
|
const keepTime = data.keep_time;
|
||||||
|
if (!issueStr || !keepTime) return false;
|
||||||
|
|
||||||
|
const issueDate = new Date(issueStr.replace(' ', 'T') + 'Z');
|
||||||
|
if (isNaN(issueDate.getTime())) return false;
|
||||||
|
|
||||||
|
_tokenExpireAt = issueDate.getTime() + keepTime * 1000;
|
||||||
|
scheduleRefresh();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('TokenManager: 过期时间估算失败', e.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 定时刷新 ---------- */
|
||||||
|
|
||||||
|
function scheduleRefresh() {
|
||||||
|
clearTimeout(_refreshTimer);
|
||||||
|
if (!_tokenExpireAt) return;
|
||||||
|
|
||||||
|
const remainingSec = (_tokenExpireAt - Date.now()) / 1000;
|
||||||
|
if (remainingSec <= 0) {
|
||||||
|
// Token 已过期,立即尝试刷新
|
||||||
|
tryRefresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 80% 位置触发,但至少等 REFRESH_MIN_GAP 秒
|
||||||
|
const delaySec = Math.max(remainingSec * (1 - REFRESH_ADVANCE), REFRESH_MIN_GAP);
|
||||||
|
_refreshTimer = setTimeout(tryRefresh, delaySec * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryRefresh() {
|
||||||
|
try {
|
||||||
|
const res = await _rawFetch('/refresh_token', { method: 'POST' });
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (data.message) console.warn('TokenManager: 刷新被拒 -', data.message);
|
||||||
|
// 冷却后重新估算
|
||||||
|
_refreshTimer = setTimeout(() => estimateExpiry(), RETRY_COOLDOWN * 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success && data.data) {
|
||||||
|
console.log(`TokenManager: 已刷新 (第${data.data.refresh_count}次, 剩余${data.data.refreshes_remaining}次)`);
|
||||||
|
}
|
||||||
|
await estimateExpiry();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('TokenManager: 刷新网络异常', e.message);
|
||||||
|
_refreshTimer = setTimeout(() => estimateExpiry(), RETRY_COOLDOWN * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 全局 fetch 401 拦截器 ---------- */
|
||||||
|
|
||||||
|
const _rawFetch = window.fetch;
|
||||||
|
|
||||||
|
window.fetch = async function (url, options) {
|
||||||
|
let resp = await _rawFetch(url, options);
|
||||||
|
|
||||||
|
// 仅处理 401;/refresh_token 自身不重试防循环
|
||||||
|
if (resp.status !== 401 || (typeof url === 'string' && url.includes('/refresh_token'))) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshRes = await _rawFetch('/refresh_token', { method: 'POST' });
|
||||||
|
if (!refreshRes.ok) return resp;
|
||||||
|
|
||||||
|
const data = await refreshRes.json();
|
||||||
|
if (!data.success) return resp;
|
||||||
|
|
||||||
|
console.log('TokenManager: 401 拦截 → 刷新成功,重试原请求');
|
||||||
|
await estimateExpiry();
|
||||||
|
|
||||||
|
// Cookie 已由 gateway Set-Cookie 自动更新,直接重试
|
||||||
|
return await _rawFetch(url, options);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('TokenManager: 401 刷新异常', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- 启动 ---------- */
|
||||||
|
|
||||||
|
estimateExpiry();
|
||||||
Reference in new issue
Block a user