mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 08:05:22 +08:00
fix: prevent expired-token login refresh loop
This commit is contained in:
@@ -114,13 +114,9 @@ export default function () {
|
|||||||
history.push('/error');
|
history.push('/error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
const responseStatus = error.response.status;
|
// Health is anonymous; a failure must not trigger a page reload loop.
|
||||||
if (responseStatus !== 401) {
|
history.push('/error');
|
||||||
history.push('/error');
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.finally(() => setInitLoading(false));
|
.finally(() => setInitLoading(false));
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-2
@@ -49,9 +49,9 @@ const errorHandler = function (
|
|||||||
if ([502, 504].includes(responseStatus)) {
|
if ([502, 504].includes(responseStatus)) {
|
||||||
history.push('/error');
|
history.push('/error');
|
||||||
} else if (responseStatus === 401) {
|
} else if (responseStatus === 401) {
|
||||||
|
localStorage.removeItem(config.authKey);
|
||||||
if (history.location.pathname !== '/login') {
|
if (history.location.pathname !== '/login') {
|
||||||
message.error(intl.get('登录已过期,请重新登录'));
|
message.error(intl.get('登录已过期,请重新登录'));
|
||||||
localStorage.removeItem(config.authKey);
|
|
||||||
history.push('/login');
|
history.push('/login');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -84,6 +84,7 @@ let _request = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const apiWhiteList = [
|
const apiWhiteList = [
|
||||||
|
`${config.baseUrl}api/health`,
|
||||||
`${config.baseUrl}api/user/login`,
|
`${config.baseUrl}api/user/login`,
|
||||||
`${config.baseUrl}open/auth/token`,
|
`${config.baseUrl}open/auth/token`,
|
||||||
`${config.baseUrl}api/user/two-factor/login`,
|
`${config.baseUrl}api/user/two-factor/login`,
|
||||||
@@ -106,8 +107,8 @@ _request.interceptors.response.use(async (response) => {
|
|||||||
if ([502, 504].includes(responseStatus)) {
|
if ([502, 504].includes(responseStatus)) {
|
||||||
history.push('/error');
|
history.push('/error');
|
||||||
} else if (responseStatus === 401) {
|
} else if (responseStatus === 401) {
|
||||||
|
localStorage.removeItem(config.authKey);
|
||||||
if (history.location.pathname !== '/login') {
|
if (history.location.pathname !== '/login') {
|
||||||
localStorage.removeItem(config.authKey);
|
|
||||||
history.push('/login');
|
history.push('/login');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const ts = require('typescript');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
function fixture(pathname = '/login', baseUrl = '/') {
|
||||||
|
const storage = new Map([['token', 'expired-token']]);
|
||||||
|
const pushes = [];
|
||||||
|
const history = { location: { pathname }, push: (p) => pushes.push(p) };
|
||||||
|
const config = { authKey: 'token', baseUrl };
|
||||||
|
const mocks = {
|
||||||
|
axios,
|
||||||
|
'react-intl-universal': { get: (s) => s },
|
||||||
|
antd: {
|
||||||
|
message: { config() {}, error() {} },
|
||||||
|
notification: { error() {} },
|
||||||
|
},
|
||||||
|
'./config': config,
|
||||||
|
'@umijs/max': { history },
|
||||||
|
'./httpError': { getErrorDetails: () => [] },
|
||||||
|
};
|
||||||
|
const source = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../src/utils/http.tsx'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const { outputText } = ts.transpileModule(source, {
|
||||||
|
compilerOptions: {
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
jsx: ts.JsxEmit.React,
|
||||||
|
esModuleInterop: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const module = { exports: {} };
|
||||||
|
new Function('require', 'module', 'exports', 'localStorage', outputText)(
|
||||||
|
(name) => {
|
||||||
|
assert.ok(Object.hasOwn(mocks, name), name);
|
||||||
|
return mocks[name];
|
||||||
|
},
|
||||||
|
module,
|
||||||
|
module.exports,
|
||||||
|
{
|
||||||
|
getItem: (k) => storage.get(k) ?? null,
|
||||||
|
removeItem: (k) => storage.delete(k),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return { request: module.exports.request, storage, pushes };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pathname of ['/login', '/dashboard']) {
|
||||||
|
test(`401 clears expired credentials on ${pathname}`, async () => {
|
||||||
|
const { request, storage, pushes } = fixture(pathname);
|
||||||
|
await assert.rejects(
|
||||||
|
request.get('/api/user', {
|
||||||
|
adapter: async (config) => {
|
||||||
|
throw new axios.AxiosError(
|
||||||
|
'Unauthorized',
|
||||||
|
'ERR_BAD_REQUEST',
|
||||||
|
config,
|
||||||
|
null,
|
||||||
|
{ status: 401, data: { message: 'expired' }, config },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.equal(storage.has('token'), false);
|
||||||
|
assert.deepEqual(pushes, pathname === '/login' ? [] : ['/login']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const baseUrl of ['/', '/panel/']) {
|
||||||
|
test(`health is anonymous while protected requests keep credentials (${baseUrl})`, async () => {
|
||||||
|
const { request } = fixture('/login', baseUrl);
|
||||||
|
const headers = [];
|
||||||
|
const adapter = async (config) => {
|
||||||
|
headers.push(config.headers.get('Authorization'));
|
||||||
|
return { status: 200, data: { code: 200 }, config };
|
||||||
|
};
|
||||||
|
await request.get(`${baseUrl}api/health`, { adapter });
|
||||||
|
await request.get(`${baseUrl}api/user`, { adapter });
|
||||||
|
assert.deepEqual(headers, [undefined, 'Bearer expired-token']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function healthFixture(get) {
|
||||||
|
const source = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../src/layouts/index.tsx'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const ast = ts.createSourceFile(
|
||||||
|
'index.tsx',
|
||||||
|
source,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true,
|
||||||
|
ts.ScriptKind.TSX,
|
||||||
|
);
|
||||||
|
let initializer;
|
||||||
|
function visit(node) {
|
||||||
|
if (
|
||||||
|
ts.isVariableDeclaration(node) &&
|
||||||
|
node.name.getText(ast) === 'getHealthStatus'
|
||||||
|
)
|
||||||
|
initializer = node.initializer;
|
||||||
|
ts.forEachChild(node, visit);
|
||||||
|
}
|
||||||
|
visit(ast);
|
||||||
|
assert.ok(initializer, 'layout health bootstrap exists');
|
||||||
|
const { outputText } = ts.transpileModule(
|
||||||
|
`const bootstrap = ${initializer.getText(ast)};`,
|
||||||
|
{
|
||||||
|
compilerOptions: { target: ts.ScriptTarget.ES2020 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const events = [];
|
||||||
|
const bootstrap = new Function(
|
||||||
|
'request',
|
||||||
|
'config',
|
||||||
|
'history',
|
||||||
|
'window',
|
||||||
|
'getSystemInfo',
|
||||||
|
'setInitLoading',
|
||||||
|
`${outputText}; return bootstrap;`,
|
||||||
|
)(
|
||||||
|
{ get },
|
||||||
|
{ apiPrefix: '/api/' },
|
||||||
|
{ push: (p) => events.push(p) },
|
||||||
|
{ location: { reload: () => events.push('reload') } },
|
||||||
|
() => events.push('system'),
|
||||||
|
(value) => events.push(['loading', value]),
|
||||||
|
);
|
||||||
|
return { bootstrap, events };
|
||||||
|
}
|
||||||
|
for (const status of [401, 503, undefined]) {
|
||||||
|
test(`health failure ${
|
||||||
|
status ?? 'network'
|
||||||
|
} settles without reloading`, async () => {
|
||||||
|
const { bootstrap, events } = healthFixture(async () => {
|
||||||
|
throw status ? { response: { status } } : new Error('Network Error');
|
||||||
|
});
|
||||||
|
bootstrap();
|
||||||
|
await new Promise(setImmediate);
|
||||||
|
assert.deepEqual(events, ['/error', ['loading', false]]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
test('healthy bootstrap continues loading system information', async () => {
|
||||||
|
const { bootstrap, events } = healthFixture(async () => ({
|
||||||
|
data: { status: 'ok' },
|
||||||
|
}));
|
||||||
|
bootstrap();
|
||||||
|
await new Promise(setImmediate);
|
||||||
|
assert.deepEqual(events, ['system', ['loading', false]]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user