# Egg.js Swagger Token 持久化教程

5 阅读3分钟

目标:在 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

  1. 点击 Authorize 输入 Bearer {token} 并确认。
  2. 刷新页面,授权状态应仍保留。

常见问题

  1. swagger-ui-bundle.jsUnexpected token '<'
    说明中间件把静态资源当 HTML 处理了,确保中间件跳过 .js/.css/.png 等路径。

  2. 找不到 swagger-persist.js
    检查:

    • app/public/swagger-persist.js 是否存在
    • config.static 是否指向 app/public
    • Swagger 页面 HTML 源码里是否包含 <script src="/public/swagger-persist.js">
  3. Token 未保存
    检查浏览器是否禁用 localStorage / 无痕模式。