Files
iot-dboard/frontend/static/js/token.js
T
2026-08-11 19:57:21 +08:00

161 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* token.js — 双 Token 认证助手
*
* - access token:存 localStorage(短期),每次请求以 `Authorization: Bearer` 发送
* - refresh token:由服务端放在 httpOnly cookie 里,前端不可读;过期时
* POST /api/auth/refresh 换取新 access(服务端同时轮换 refresh cookie)
* - 401 处理:并发 401 共享一次刷新(single-flight),刷新成功后重试一次原请求;
* 刷新失败则清空本地 token 并跳转登录页
* - SSE(EventSource 无法带自定义 header)通过一次性 ticket 建立,见 display.js
*/
(function (global) {
'use strict';
var KEY = 'dashboard_access_token';
function getAccess() {
try { return localStorage.getItem(KEY) || ''; } catch (e) { return ''; }
}
function setAccess(t) {
try {
if (t) { localStorage.setItem(KEY, t); } else { localStorage.removeItem(KEY); }
} catch (e) {}
}
function clear() { setAccess(''); }
function _detectView() {
var p = location.pathname;
if (p.indexOf('/control') === 0 || p.indexOf('/mfa-setup') === 0) return 'control';
return 'display';
}
function loginRedirect(view) {
clear();
var next = encodeURIComponent(location.pathname + location.search);
location.href = '/login?view=' + (view || _detectView()) + '&next=' + next;
}
// 并发 401 共享同一次刷新(避免同时打多个 refresh 导致 token 轮换竞争)
var _refreshing = null;
function refresh() {
if (!_refreshing) {
_refreshing = new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/auth/refresh');
xhr.onload = function () {
if (xhr.status === 200) {
try {
var d = JSON.parse(xhr.responseText || '{}');
if (d.access_token) { setAccess(d.access_token); resolve(d.access_token); return; }
} catch (e) {}
}
reject(new Error('refresh-failed'));
};
xhr.onerror = function () { reject(new Error('network')); };
xhr.send();
});
_refreshing.then(function () { _refreshing = null; },
function () { _refreshing = null; });
}
return _refreshing;
}
/**
* 统一请求封装(XHR,回调风格)
* request(method, url, data, callback, opts)
* - data: 对象或 FormData;null/undefined 表示无 body
* - opts.view: 跳转登录页时使用的 view(默认按当前路径推断)
* - callback(res, status): 收到响应(含 401 重试后的最终结果)
*/
function request(method, url, data, callback, opts) {
opts = opts || {};
var view = opts.view || _detectView();
function doSend(access, retried) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
if (access) xhr.setRequestHeader('Authorization', 'Bearer ' + access);
if (data && !(data instanceof FormData)) {
xhr.setRequestHeader('Content-Type', 'application/json');
}
xhr.onload = function () {
var res = {};
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) {}
if (xhr.status === 401) {
if (!retried) {
var fail = function () {
if (opts.noRedirect) { if (callback) callback(res, xhr.status); return; }
// 多 tab 竞态:另一个 tab 可能刚轮换过共享的 refresh cookie,
// 短暂延迟后用新 cookie 再试一次,仍失败才判定会话失效。
setTimeout(function () {
refresh().then(function () {
doSend(getAccess(), true);
}, function () {
loginRedirect(view);
});
}, 300);
};
refresh().then(function () {
doSend(getAccess(), true);
}, fail);
return;
}
// 刷新后重试仍 401 → 会话确实失效(如已被踢),跳转登录页
if (!opts.noRedirect) {
loginRedirect(view);
return;
}
if (callback) callback(res, xhr.status);
return;
}
if (callback) callback(res, xhr.status);
};
xhr.onerror = function () { if (callback) callback({}, 0); };
if (data instanceof FormData) xhr.send(data);
else if (data) xhr.send(JSON.stringify(data));
else xhr.send();
}
doSend(getAccess(), false);
}
/** Promise 风格的 fetch 封装(带 Authorization,用于登录页/Passkey 桥等) */
function fetchWithAuth(url, options) {
options = options || {};
var access = getAccess();
var headers = Object.assign({}, options.headers || {});
if (access && !headers['Authorization']) headers['Authorization'] = 'Bearer ' + access;
return fetch(url, Object.assign({}, options, {
headers: headers,
credentials: 'same-origin',
}));
}
/** 主动登出:清本地 access + 通知服务端吊销 refresh cookie(同时吊销本 access) */
function logout(cb) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/auth/logout');
var access = getAccess();
if (access) xhr.setRequestHeader('Authorization', 'Bearer ' + access);
xhr.onload = function () {
clear();
if (cb) cb();
};
xhr.onerror = function () {
clear();
if (cb) cb();
};
xhr.send();
}
global.Auth = {
getAccess: getAccess,
setAccess: setAccess,
clear: clear,
refresh: refresh,
request: request,
fetchWithAuth: fetchWithAuth,
logout: logout,
loginRedirect: loginRedirect,
};
})(window);