目标:在 Swagger UI 刷新后仍保留 Authorization token。
1) 依赖安装
已安装可跳过。
npm i egg-swagger-doc
2) 启用插件
编辑 config/plugin.js:
exports.swaggerdoc = {
enable: true,
package: 'egg-swagger-doc',
};
3) 配置 Swagger 与中间件
编辑 config/config.default.js:
config.middleware = ['swaggerPersist'];
config.swaggerPersist = {
storageKey: 'swagger_auth_token',
legacyStorageKey: 'swagger_authorization_token',
};
config.swaggerdoc = {
enable: true,
routerMap: true,
dirScanner: './app/controller',
apiInfo: {
title: 'API',
description: 'API Docs',
version: '1.0.0',
},
enableSecurity: true,
securityDefinitions: {
apikey: {
type: 'apiKey',
name: 'Authorization',
in: 'header',
description: 'Bearer {token}',
},
},
swaggerOptions: {
persistAuthorization: true,
},
};
4) 添加中间件(注入持久化脚本)
创建 app/middleware/swagger_persist.js:
// Injects Swagger token persistence script into Swagger UI HTML.
module.exports = () => {
return async function swaggerPersist(ctx, next) {
await next();
const swaggerdoc = ctx.app.config.swaggerdoc || {};
const configPath = swaggerdoc.path;
const swaggerPaths = [configPath, '/swagger-ui', '/swagger-ui.html', '/swagger.html', '/doc', '/docs'].filter(Boolean);
const isSwaggerPath = swaggerPaths.some((p) => ctx.path === p || ctx.path.startsWith(`${p}/`));
const isStaticAsset = /\.(js|css|png|jpg|jpeg|gif|svg|woff2?|ttf|ico)$/i.test(ctx.path || '');
if (!ctx.body || isStaticAsset) {
return;
}
let bodyText = null;
if (typeof ctx.body === 'string') {
bodyText = ctx.body;
} else if (Buffer.isBuffer(ctx.body)) {
bodyText = ctx.body.toString('utf8');
} else if (ctx.body && typeof ctx.body.pipe === 'function') {
const chunks = [];
for await (const chunk of ctx.body) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
bodyText = Buffer.concat(chunks).toString('utf8');
}
if (!bodyText) {
return;
}
const isLikelyHtml = /<!doctype html/i.test(bodyText) || /<html\\b/i.test(bodyText) || /<body\\b/i.test(bodyText);
if (!isLikelyHtml) {
ctx.body = bodyText;
return;
}
const looksLikeSwagger =
isSwaggerPath || /SwaggerUIBundle\\s*\\(/.test(bodyText) || /swagger-ui-bundle\\.js/.test(bodyText);
if (!looksLikeSwagger) {
ctx.body = bodyText;
return;
}
ctx.set('Cache-Control', 'no-store');
if (!/persistAuthorization\\s*:/.test(bodyText)) {
const bundleMarker = 'SwaggerUIBundle({';
if (bodyText.includes(bundleMarker)) {
bodyText = bodyText.replace(bundleMarker, `${bundleMarker}\\n persistAuthorization: true,`);
}
}
const securityDefinitions = swaggerdoc.securityDefinitions || {};
const securityKey = Object.keys(securityDefinitions)[0] || 'apikey';
const authHeaderName = (securityDefinitions[securityKey] && securityDefinitions[securityKey].name) || 'Authorization';
const storageKey = (ctx.app.config.swaggerPersist && ctx.app.config.swaggerPersist.storageKey) || 'swagger_auth_token';
const legacyStorageKey =
(ctx.app.config.swaggerPersist && ctx.app.config.swaggerPersist.legacyStorageKey) || 'swagger_authorization_token';
const configScript = `<script>window.__SWAGGER_PERSIST__=${JSON.stringify({
securityKey,
authHeaderName,
storageKey,
legacyStorageKey,
})};</script>`;
const version = (ctx.app.config.swaggerPersist && ctx.app.config.swaggerPersist.version) || Date.now().toString();
const customScript = `${configScript}\\n<script src="/public/swagger-persist.js?v=${version}"></script>\\n`;
const bodyCloseTag = /<\\/body>/i;
if (bodyCloseTag.test(bodyText)) {
ctx.body = bodyText.replace(bodyCloseTag, customScript + '</body>');
} else {
ctx.body = bodyText + customScript;
}
ctx.remove('Content-Length');
};
};
5) 添加前端持久化脚本
创建 app/public/swagger-persist.js:
// Swagger token persistence for Swagger UI.
(function () {
'use strict';
var config = window.__SWAGGER_PERSIST__ || {};
var TOKEN_STORAGE_KEY = config.storageKey || 'swagger_auth_token';
var SECURITY_KEY = config.securityKey || 'apikey';
var AUTH_HEADER = config.authHeaderName || 'Authorization';
var LEGACY_STORAGE_KEY = config.legacyStorageKey || 'swagger_authorization_token';
var AUTH_STATE_KEYS = ['authorized', 'swagger-ui'];
function safeLocalStorageGet(key) {
try {
return localStorage.getItem(key);
} catch (e) {
return null;
}
}
function safeLocalStorageSet(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (e) {
return false;
}
}
function safeSessionStorageGet(key) {
try {
return sessionStorage.getItem(key);
} catch (e) {
return null;
}
}
function safeSessionStorageSet(key, value) {
try {
sessionStorage.setItem(key, value);
return true;
} catch (e) {
return false;
}
}
function storageGet(key) {
return safeLocalStorageGet(key) || safeSessionStorageGet(key);
}
function storageSet(key, value) {
if (safeLocalStorageSet(key, value)) return;
safeSessionStorageSet(key, value);
}
function normalizeToken(raw) {
if (!raw) return '';
var trimmed = String(raw).trim();
if (!trimmed) return '';
return /^bearer\\s+/i.test(trimmed) ? trimmed : 'Bearer ' + trimmed;
}
function saveToken(raw) {
var token = normalizeToken(raw);
if (token) {
storageSet(TOKEN_STORAGE_KEY, token);
}
}
function getStoredToken() {
var token = storageGet(TOKEN_STORAGE_KEY);
if (!token && LEGACY_STORAGE_KEY) {
token = storageGet(LEGACY_STORAGE_KEY);
if (token) {
storageSet(TOKEN_STORAGE_KEY, token);
}
}
return token;
}
function extractTokenFromAuthorized(authorized) {
if (!authorized) return '';
var data = authorized;
try {
if (authorized.toJS) {
data = authorized.toJS();
}
} catch (e) {
// ignore
}
var entry = null;
if (data && typeof data === 'object') {
if (data[SECURITY_KEY]) {
entry = data[SECURITY_KEY];
} else {
for (var key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
if (data[key] && (data[key].value || data[key].token || data[key].access_token)) {
entry = data[key];
break;
}
}
}
}
if (!entry) return '';
if (entry.value) return entry.value;
if (entry.token && entry.token.access_token) return entry.token.access_token;
if (entry.access_token) return entry.access_token;
if (entry.apiKey) return entry.apiKey;
return '';
}
function readAuthorizedFromStorage() {
for (var i = 0; i < AUTH_STATE_KEYS.length; i++) {
var key = AUTH_STATE_KEYS[i];
var raw = safeLocalStorageGet(key);
if (!raw) continue;
try {
return JSON.parse(raw);
} catch (e) {
// ignore
}
}
return null;
}
function persistFromSystem(system) {
if (!system || !system.authSelectors || !system.authSelectors.authorized) return;
var authorized = system.authSelectors.authorized();
var token = extractTokenFromAuthorized(authorized);
if (!token) {
var storedAuth = readAuthorizedFromStorage();
token = extractTokenFromAuthorized(storedAuth);
}
if (token) {
saveToken(token);
}
}
function restoreToken(ui) {
var token = getStoredToken();
if (!token || !ui) return;
try {
if (typeof ui.preauthorizeApiKey === 'function') {
ui.preauthorizeApiKey(SECURITY_KEY, token);
return;
}
var system = ui.getSystem && ui.getSystem();
var actions = (system && system.authActions) || ui.authActions;
if (actions && typeof actions.authorize === 'function') {
var payload = {};
payload[SECURITY_KEY] = {
name: AUTH_HEADER,
schema: { type: 'apiKey', in: 'header', name: AUTH_HEADER },
value: token,
};
actions.authorize(payload);
}
} catch (e) {
// ignore
}
}
function wrapAuthActions(ui, system) {
var target = null;
var actions = null;
if (system && system.authActions) {
target = system;
actions = system.authActions;
} else if (ui && ui.authActions) {
target = ui;
actions = ui.authActions;
}
if (!actions || target.__swaggerPersistWrapped) return;
target.__swaggerPersistWrapped = true;
var originalAuthorize = actions.authorize;
if (typeof originalAuthorize === 'function') {
actions.authorize = function (payload) {
var entry = payload && payload[SECURITY_KEY];
var token =
(entry && entry.value) ||
(payload && payload.value) ||
(payload && payload.token) ||
extractTokenFromAuthorized(payload);
if (token) {
saveToken(token);
}
var result = originalAuthorize(payload);
if (system) {
setTimeout(function () {
persistFromSystem(system);
}, 0);
}
return result;
};
}
}
function initWhenReady() {
if (window.ui) {
if (typeof window.ui.getSystem === 'function') {
var system = window.ui.getSystem();
wrapAuthActions(window.ui, system);
try {
var store = system.getStore && system.getStore();
if (store && typeof store.subscribe === 'function') {
store.subscribe(function () {
persistFromSystem(system);
});
}
} catch (e) {
// ignore
}
persistFromSystem(system);
}
restoreToken(window.ui);
return;
}
setTimeout(initWhenReady, 300);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWhenReady);
} else {
initWhenReady();
}
})();
6) 启动与验证
npm run dev
访问 http://localhost:7001/swagger-ui.html:
- 点击 Authorize 输入
Bearer {token}并确认。 - 刷新页面,授权状态应仍保留。
常见问题
-
swagger-ui-bundle.js报Unexpected token '<'
说明中间件把静态资源当 HTML 处理了,确保中间件跳过.js/.css/.png等路径。 -
找不到
swagger-persist.js
检查:app/public/swagger-persist.js是否存在config.static是否指向app/public- Swagger 页面 HTML 源码里是否包含
<script src="/public/swagger-persist.js">
-
Token 未保存
检查浏览器是否禁用 localStorage / 无痕模式。