修复 openapi 鉴权提示

This commit is contained in:
whyour 2026-06-26 15:31:25 +08:00
parent 3044f63f03
commit 5fbff0e1c8
2 changed files with 28 additions and 18 deletions

View File

@ -12,6 +12,7 @@ import { IKeyvStore, shareStore } from '../shared/store';
import { isValidToken } from '../shared/auth'; import { isValidToken } from '../shared/auth';
import path from 'path'; import path from 'path';
import { t } from '../shared/i18n'; import { t } from '../shared/i18n';
import { AppScope } from '../data/open';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
// Security: Enable strict routing to prevent case-insensitive path bypass // Security: Enable strict routing to prevent case-insensitive path bypass
@ -19,30 +20,30 @@ export default ({ app }: { app: Application }) => {
app.set('strict routing', true); app.set('strict routing', true);
app.set('trust proxy', 'loopback'); app.set('trust proxy', 'loopback');
app.use(cors()); app.use(cors());
// Security: Path normalization middleware to prevent case variation attacks // Security: Path normalization middleware to prevent case variation attacks
app.use((req, res, next) => { app.use((req, res, next) => {
const originalPath = req.path; const originalPath = req.path;
const normalizedPath = originalPath.toLowerCase(); const normalizedPath = originalPath.toLowerCase();
// Block requests with case variations on protected paths // Block requests with case variations on protected paths
if (originalPath !== normalizedPath && if (originalPath !== normalizedPath &&
(normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) { (normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) {
return res.status(400).json({ return res.status(400).json({
code: 400, code: 400,
message: 'Invalid path format' message: 'Invalid path format'
}); });
} }
next(); next();
}); });
// Rewrite URLs to strip baseUrl prefix if configured // Rewrite URLs to strip baseUrl prefix if configured
// This allows the rest of the app to work without baseUrl awareness // This allows the rest of the app to work without baseUrl awareness
if (config.baseUrl) { if (config.baseUrl) {
app.use(rewrite(`${config.baseUrl}/*`, '/$1')); app.use(rewrite(`${config.baseUrl}/*`, '/$1'));
} }
app.get(`${config.api.prefix}/env.js`, serveEnv); app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath)); app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
@ -87,17 +88,26 @@ export default ({ app }: { app: Application }) => {
const currentToken = doc.tokens.find((x) => x.value === headerToken); const currentToken = doc.tokens.find((x) => x.value === headerToken);
const keyMatch = pathLower.match(/\/open\/([a-z]+)\/*/); const keyMatch = pathLower.match(/\/open\/([a-z]+)\/*/);
const key = keyMatch && keyMatch[1]; const key = keyMatch && keyMatch[1];
if (
doc.scopes.includes(key as any) && if (!doc.scopes.includes(key as AppScope)) {
currentToken && const err = new UnauthorizedError('credentials_bad_scheme', {
currentToken.expiration >= Math.round(Date.now() / 1000) message: t('暂无权限'),
) { });
return next(); return next(err);
} }
if (!currentToken || currentToken.expiration < Math.round(Date.now() / 1000)) {
const err = new UnauthorizedError('invalid_token', {
message: t('Token 已失效'),
});
return next(err);
}
return next();
} }
} }
const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`; const originPath = `${req.baseUrl}${pathLower === '/' ? '' : pathLower}`;
if ( if (
!headerToken && !headerToken &&
originPath && originPath &&
@ -113,8 +123,8 @@ export default ({ app }: { app: Application }) => {
const errorCode = headerToken ? 'invalid_token' : 'credentials_required'; const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
const errorMessage = headerToken const errorMessage = headerToken
? 'jwt malformed' ? t('Token 已失效')
: 'No authorization token was found'; : t('请先登录');
const err = new UnauthorizedError(errorCode, { message: errorMessage }); const err = new UnauthorizedError(errorCode, { message: errorMessage });
next(err); next(err);
}); });
@ -127,7 +137,7 @@ export default ({ app }: { app: Application }) => {
'/api/user/notification/init', '/api/user/notification/init',
'/open/user/init', '/open/user/init',
'/open/user/notification/init', '/open/user/notification/init',
].includes(req.path) ].includes(pathLower)
) { ) {
return next(); return next();
} }

View File

@ -82,7 +82,7 @@ const messages: Record<string, Record<string, string>> = {
'Url 或者 Body 中必须包含 $title': 'Url or Body must contain $title', 'Url 或者 Body 中必须包含 $title': 'Url or Body must contain $title',
'绝对路径必须在日志目录内或使用 /dev/null': '绝对路径必须在日志目录内或使用 /dev/null':
'Absolute path must be within log directory or use /dev/null', 'Absolute path must be within log directory or use /dev/null',
'请先登录!': 'Please login first!', '请先登录': 'Please login first',
'运行中...': 'Running...', '运行中...': 'Running...',
'日志不存在...': 'Log does not exist...', '日志不存在...': 'Log does not exist...',
'未分类': 'Uncategorized', '未分类': 'Uncategorized',