feat(auth): 将token存储从内存改为本地文件存储

修改了认证系统的token存储机制,从内存Map改为本地JSON文件存储,提高了token的持久化能力。同时更新了启动脚本,简化了错误处理逻辑,并在UI中添加了高亮说明样式和提供商池配置的描述信息。

- 实现了基于文件的token存储、读取、删除和清理功能
- 所有token相关操作改为异步处理
- 添加了highlight-note样式类用于重要信息提示
- 更新了提供商池配置的说明文案
This commit is contained in:
hex2077 2025-11-23 18:57:13 +08:00
parent 9b5b5810e3
commit 8a782c49f0
5 changed files with 111 additions and 35 deletions

View file

@ -88,14 +88,4 @@ echo ⏹️ 按 Ctrl+C 停止服务器
echo.
:: 启动服务器
node src\api-server.js
:: 如果启动失败
if !errorlevel! neq 0 (
echo.
echo ❌ 服务器异常
pause
exit /b 1
)
pause
node src\api-server.js

View file

@ -91,12 +91,4 @@ echo "⏹️ 按 Ctrl+C 停止服务器"
echo
# 启动服务器
node src/api-server.js
# 如果启动失败
if [ $? -ne 0 ]; then
echo
echo "❌ 服务器异常"
echo "请检查错误信息并重试"
exit 1
fi
node src/api-server.js

View file

@ -8,8 +8,38 @@ import { CONFIG } from './config-manager.js';
import { serviceInstances } from './adapter.js';
import { initApiService } from './service-manager.js';
// Token存储在内存中生产环境建议使用Redis
const tokenStore = new Map();
// Token存储到本地文件中
const TOKEN_STORE_FILE = 'token-store.json';
/**
* 读取token存储文件
*/
async function readTokenStore() {
try {
if (existsSync(TOKEN_STORE_FILE)) {
const content = await fs.readFile(TOKEN_STORE_FILE, 'utf8');
return JSON.parse(content);
} else {
// 如果文件不存在创建一个默认的token store
await writeTokenStore({ tokens: {} });
return { tokens: {} };
}
} catch (error) {
console.error('读取token存储文件失败:', error);
return { tokens: {} };
}
}
/**
* 写入token存储文件
*/
async function writeTokenStore(tokenStore) {
try {
await fs.writeFile(TOKEN_STORE_FILE, JSON.stringify(tokenStore, null, 2), 'utf8');
} catch (error) {
console.error('写入token存储文件失败:', error);
}
}
/**
* 生成简单的token
@ -30,31 +60,60 @@ function getExpiryTime() {
/**
* 验证简单token
*/
function verifyToken(token) {
const tokenInfo = tokenStore.get(token);
async function verifyToken(token) {
const tokenStore = await readTokenStore();
const tokenInfo = tokenStore.tokens[token];
if (!tokenInfo) {
return null;
}
// 检查是否过期
if (Date.now() > tokenInfo.expiryTime) {
tokenStore.delete(token);
await deleteToken(token);
return null;
}
return tokenInfo;
}
/**
* 保存token到本地文件
*/
async function saveToken(token, tokenInfo) {
const tokenStore = await readTokenStore();
tokenStore.tokens[token] = tokenInfo;
await writeTokenStore(tokenStore);
}
/**
* 删除token
*/
async function deleteToken(token) {
const tokenStore = await readTokenStore();
if (tokenStore.tokens[token]) {
delete tokenStore.tokens[token];
await writeTokenStore(tokenStore);
}
}
/**
* 清理过期的token
*/
function cleanupExpiredTokens() {
async function cleanupExpiredTokens() {
const tokenStore = await readTokenStore();
const now = Date.now();
for (const [token, info] of tokenStore.entries()) {
if (now > info.expiryTime) {
tokenStore.delete(token);
let hasChanges = false;
for (const token in tokenStore.tokens) {
if (now > tokenStore.tokens[token].expiryTime) {
delete tokenStore.tokens[token];
hasChanges = true;
}
}
if (hasChanges) {
await writeTokenStore(tokenStore);
}
}
/**
@ -105,7 +164,7 @@ function parseRequestBody(req) {
/**
* 检查token验证
*/
function checkAuth(req) {
async function checkAuth(req) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
@ -113,7 +172,7 @@ function checkAuth(req) {
}
const token = authHeader.substring(7);
const tokenInfo = verifyToken(token);
const tokenInfo = await verifyToken(token);
return tokenInfo !== null;
}
@ -145,8 +204,8 @@ async function handleLoginRequest(req, res) {
const token = generateToken();
const expiryTime = getExpiryTime();
// 存储token信息
tokenStore.set(token, {
// 存储token信息到本地文件
await saveToken(token, {
username: 'admin',
loginTime: Date.now(),
expiryTime
@ -301,7 +360,8 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
// Handle UI management API requests (需要token验证除了登录接口、健康检查和Events接口)
if (pathParam.startsWith('/api/') && pathParam !== '/api/login' && pathParam !== '/api/health' && pathParam !== '/api/events') {
// 检查token验证
if (!checkAuth(req)) {
const isAuth = await checkAuth(req);
if (!isAuth) {
res.writeHead(401, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',

View file

@ -120,6 +120,33 @@ body {
font-size: 14px;
}
/* 高亮说明样式 */
.highlight-note {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 1px solid #fbbf24;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
color: #92400e;
font-weight: 500;
width: 100%;
box-sizing: border-box;
}
.highlight-note i {
color: #f59e0b;
font-size: 1.25rem;
flex-shrink: 0;
}
.highlight-note span {
flex: 1;
text-align: center;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
@ -2956,4 +2983,5 @@ input:checked + .toggle-slider:before {
.contact-section h3 i {
color: var(--primary-color);
}
}

View file

@ -625,7 +625,7 @@
<div class="form-group pool-section">
<label for="providerPoolsFilePath">提供商池配置文件路径</label>
<input type="text" id="providerPoolsFilePath" class="form-control" value="provider_pools.json" placeholder="例如: provider_pools.json">
<small class="form-text">配置了提供商池后,可在提供商池管理中查看详细信息</small>
<small class="form-text">配置了提供商池后,默认使用提供商池的配置,提供商池配置失效降级到默认配置</small>
</div>
<div class="form-group pool-section">
@ -708,6 +708,12 @@
<!-- Provider Pools Section -->
<section id="providers" class="section" aria-labelledby="providers-title">
<h2 id="providers-title">提供商池管理</h2>
<div class="pool-description">
<div class="highlight-note">
<i class="fas fa-info-circle"></i>
<span>配置了提供商池后,默认使用提供商池的配置,提供商池配置失效降级到默认配置</span>
</div>
</div>
<!-- Provider Pool Stats -->
<div class="stats-grid">
<div class="stat-card">