feat(auth): 将token存储从内存改为本地文件存储
修改了认证系统的token存储机制,从内存Map改为本地JSON文件存储,提高了token的持久化能力。同时更新了启动脚本,简化了错误处理逻辑,并在UI中添加了高亮说明样式和提供商池配置的描述信息。 - 实现了基于文件的token存储、读取、删除和清理功能 - 所有token相关操作改为异步处理 - 添加了highlight-note样式类用于重要信息提示 - 更新了提供商池配置的说明文案
This commit is contained in:
parent
9b5b5810e3
commit
8a782c49f0
5 changed files with 111 additions and 35 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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': '*',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue