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 =====
// 获取当前用户信息