feat: 实现Token刷新机制,缩短登录有效期

安全改进:
- Access Token有效期从7天缩短为2小时
- 添加Refresh Token机制(有效期7天)
- 关闭浏览器后较快失效,提升安全性

后端修改(auth.js):
- 添加generateRefreshToken函数生成刷新令牌
- 添加refreshAccessToken函数验证并刷新access token
- 分离ACCESS_TOKEN_EXPIRES和REFRESH_TOKEN_EXPIRES配置

后端修改(server.js):
- 登录时返回refreshToken和expiresIn
- 添加/api/refresh-token接口用于刷新token
- Cookie有效期同步调整为2小时

前端修改(app.js):
- 保存refreshToken到localStorage
- 添加自动刷新定时器(过期前5分钟刷新)
- 页面加载时若token过期自动尝试刷新
- 登出时清除refreshToken和定时器

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-28 13:46:51 +08:00
parent 1d65e97b04
commit 540c292d70
3 changed files with 211 additions and 9 deletions

View File

@@ -1,8 +1,15 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const { UserDB } = require('./database');
// JWT密钥必须在环境变量中设置
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
// Refresh Token密钥使用不同的密钥
const REFRESH_SECRET = process.env.REFRESH_SECRET || JWT_SECRET + '-refresh';
// Token有效期配置
const ACCESS_TOKEN_EXPIRES = '2h'; // Access token 2小时
const REFRESH_TOKEN_EXPIRES = '7d'; // Refresh token 7天
// 安全检查验证JWT密钥配置
const DEFAULT_SECRETS = [
@@ -36,19 +43,77 @@ if (DEFAULT_SECRETS.includes(JWT_SECRET)) {
console.log('[安全] JWT密钥已配置');
// 生成JWT Token
// 生成Access Token(短期)
function generateToken(user) {
return jwt.sign(
{
id: user.id,
username: user.username,
is_admin: user.is_admin
is_admin: user.is_admin,
type: 'access'
},
JWT_SECRET,
{ expiresIn: '7d' }
{ expiresIn: ACCESS_TOKEN_EXPIRES }
);
}
// 生成Refresh Token长期
function generateRefreshToken(user) {
return jwt.sign(
{
id: user.id,
type: 'refresh',
// 添加随机标识使每次生成的refresh token不同
jti: crypto.randomBytes(16).toString('hex')
},
REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRES }
);
}
// 验证Refresh Token并返回新的Access Token
function refreshAccessToken(refreshToken) {
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
if (decoded.type !== 'refresh') {
return { success: false, message: '无效的刷新令牌类型' };
}
const user = UserDB.findById(decoded.id);
if (!user) {
return { success: false, message: '用户不存在' };
}
if (user.is_banned) {
return { success: false, message: '账号已被封禁' };
}
if (!user.is_active) {
return { success: false, message: '账号未激活' };
}
// 生成新的access token
const newAccessToken = generateToken(user);
return {
success: true,
token: newAccessToken,
user: {
id: user.id,
username: user.username,
is_admin: user.is_admin
}
};
} catch (error) {
if (error.name === 'TokenExpiredError') {
return { success: false, message: '刷新令牌已过期,请重新登录' };
}
return { success: false, message: '无效的刷新令牌' };
}
}
// 验证Token中间件
function authMiddleware(req, res, next) {
// 从请求头或HttpOnly Cookie获取token不再接受URL参数以避免泄露
@@ -142,7 +207,11 @@ function isJwtSecretSecure() {
module.exports = {
JWT_SECRET,
generateToken,
generateRefreshToken,
refreshAccessToken,
authMiddleware,
adminMiddleware,
isJwtSecretSecure
isJwtSecretSecure,
ACCESS_TOKEN_EXPIRES,
REFRESH_TOKEN_EXPIRES
};

View File

@@ -21,7 +21,7 @@ const execAsync = util.promisify(exec);
const execFileAsync = util.promisify(execFile);
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB } = require('./database');
const { generateToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth');
const { generateToken, generateRefreshToken, refreshAccessToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth');
const app = express();
const PORT = process.env.PORT || 40001;
@@ -1632,6 +1632,7 @@ app.post('/api/login',
}
const token = generateToken(user);
const refreshToken = generateRefreshToken(user);
// 清除失败记录
if (req.rateLimitKeys) {
@@ -1648,7 +1649,7 @@ app.post('/api/login',
secure: isSecureEnv,
// HTTPS环境使用strictHTTP环境使用lax开发环境兼容
sameSite: isSecureEnv ? 'strict' : 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
maxAge: 2 * 60 * 60 * 1000, // 2小时与access token有效期一致
path: '/' // 限制Cookie作用域
});
@@ -1659,6 +1660,8 @@ app.post('/api/login',
success: true,
message: '登录成功',
token,
refreshToken, // 返回refresh token
expiresIn: 2 * 60 * 60 * 1000, // 告知前端access token有效期毫秒
user: {
id: user.id,
username: user.username,
@@ -1683,6 +1686,43 @@ app.post('/api/login',
}
);
// 刷新Access Token
app.post('/api/refresh-token', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
success: false,
message: '缺少刷新令牌'
});
}
const result = refreshAccessToken(refreshToken);
if (!result.success) {
return res.status(401).json({
success: false,
message: result.message
});
}
// 更新Cookie中的token
const isSecureEnv = process.env.COOKIE_SECURE === 'true';
res.cookie('token', result.token, {
httpOnly: true,
secure: isSecureEnv,
sameSite: isSecureEnv ? 'strict' : 'lax',
maxAge: 2 * 60 * 60 * 1000, // 2小时
path: '/'
});
res.json({
success: true,
token: result.token,
expiresIn: 2 * 60 * 60 * 1000
});
});
// ===== 需要认证的API =====
// 获取当前用户信息

View File

@@ -11,6 +11,8 @@ createApp({
isLoggedIn: false,
user: null,
token: null,
refreshToken: null,
tokenRefreshTimer: null,
// 视图状态
currentView: 'files',
@@ -523,6 +525,7 @@ handleDragLeave(e) {
if (response.data.success) {
this.token = response.data.token;
this.refreshToken = response.data.refreshToken;
this.user = response.data.user;
this.isLoggedIn = true;
this.showResendVerify = false;
@@ -534,8 +537,13 @@ handleDragLeave(e) {
// 保存token到localStorage
localStorage.setItem('token', this.token);
localStorage.setItem('refreshToken', this.refreshToken);
localStorage.setItem('user', JSON.stringify(this.user));
// 启动token自动刷新在过期前5分钟刷新
const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000);
this.startTokenRefresh(expiresIn);
// 直接从登录响应中获取存储信息
this.storagePermission = this.user.storage_permission || 'sftp_only';
this.storageType = this.user.current_storage_type || 'sftp';
@@ -976,7 +984,10 @@ handleDragLeave(e) {
this.isLoggedIn = false;
this.user = null;
this.token = null;
this.refreshToken = null;
this.stopTokenRefresh();
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
localStorage.removeItem('lastView');
this.showResendVerify = false;
@@ -1002,10 +1013,12 @@ handleDragLeave(e) {
// 检查本地存储的登录状态
async checkLoginStatus() {
const token = localStorage.getItem('token');
const refreshToken = localStorage.getItem('refreshToken');
const user = localStorage.getItem('user');
if (token && user) {
this.token = token;
this.refreshToken = refreshToken;
this.user = JSON.parse(user);
// 先验证token是否有效
@@ -1031,6 +1044,9 @@ handleDragLeave(e) {
console.log('[页面加载] Token验证成功存储权限:', this.storagePermission, '存储类型:', this.storageType);
// 启动token自动刷新假设剩余1.5小时,实际由服务端控制)
this.startTokenRefresh(1.5 * 60 * 60 * 1000);
// 启动定期检查用户配置
this.startProfileSync();
// 加载用户主题设置
@@ -1052,29 +1068,106 @@ handleDragLeave(e) {
// 强制切换到目标视图并加载数据
this.switchView(targetView, true);
} else {
// 响应异常,清除登录状态
this.handleTokenExpired();
// 响应异常,尝试刷新token
await this.tryRefreshOrLogout();
}
} catch (error) {
console.warn('[页面加载] Token验证失败:', error.response?.status || error.message);
// token无效或过期清除登录状态
// token无效或过期尝试使用refresh token刷新
if (error.response?.status === 401 && this.refreshToken) {
console.log('[页面加载] 尝试使用refresh token刷新...');
const refreshed = await this.doRefreshToken();
if (refreshed) {
// 刷新成功,重新检查登录状态
await this.checkLoginStatus();
return;
}
}
// 刷新失败或无refresh token清除登录状态
this.handleTokenExpired();
}
}
},
// 尝试刷新token失败则登出
async tryRefreshOrLogout() {
if (this.refreshToken) {
const refreshed = await this.doRefreshToken();
if (refreshed) {
await this.checkLoginStatus();
return;
}
}
this.handleTokenExpired();
},
// 处理token过期/失效
handleTokenExpired() {
console.log('[认证] Token已失效清除登录状态');
this.isLoggedIn = false;
this.user = null;
this.token = null;
this.refreshToken = null;
this.stopTokenRefresh();
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
localStorage.removeItem('lastView');
this.stopProfileSync();
},
// 启动token自动刷新定时器
startTokenRefresh(expiresIn) {
this.stopTokenRefresh(); // 先清除旧的定时器
// 在token过期前5分钟刷新
const refreshTime = Math.max(expiresIn - 5 * 60 * 1000, 60 * 1000);
console.log(`[认证] Token将在 ${Math.round(refreshTime / 60000)} 分钟后刷新`);
this.tokenRefreshTimer = setTimeout(async () => {
await this.doRefreshToken();
}, refreshTime);
},
// 停止token刷新定时器
stopTokenRefresh() {
if (this.tokenRefreshTimer) {
clearTimeout(this.tokenRefreshTimer);
this.tokenRefreshTimer = null;
}
},
// 执行token刷新
async doRefreshToken() {
if (!this.refreshToken) {
console.log('[认证] 无refresh token无法刷新');
return false;
}
try {
console.log('[认证] 正在刷新access token...');
const response = await axios.post(`${this.apiBase}/api/refresh-token`, {
refreshToken: this.refreshToken
});
if (response.data.success) {
this.token = response.data.token;
localStorage.setItem('token', this.token);
console.log('[认证] Token刷新成功');
// 继续下一次刷新
const expiresIn = response.data.expiresIn || (2 * 60 * 60 * 1000);
this.startTokenRefresh(expiresIn);
return true;
}
} catch (error) {
console.error('[认证] Token刷新失败:', error.response?.data?.message || error.message);
// 刷新失败,需要重新登录
this.handleTokenExpired();
}
return false;
},
// 检查URL参数
checkUrlParams() {
const urlParams = new URLSearchParams(window.location.search);