This commit is contained in:
张成
2025-12-19 22:24:23 +08:00
parent abe2ae3c3a
commit 10aff2f266
12 changed files with 1101 additions and 147 deletions

View File

@@ -77,92 +77,163 @@ module.exports = {
"POST /user/login": async (ctx) => {
const { phone, password, device_id: client_device_id } = ctx.getBody();
const dayjs = require('dayjs');
const { verifyPassword, validateDeviceId, maskPhone } = require('../utils/crypto_utils');
const { isAuthorizationValid } = require('../utils/account_utils');
// 验证参数
// 参数验证
if (!phone || !password) {
return ctx.fail('手机号和密码不能为空');
}
// 验证手机号格式
const phonePattern = /^1[3-9]\d{9}$/;
if (!phonePattern.test(phone)) {
return ctx.fail('手机号格式不正确');
}
// 验证密码长度
if (password.length < 6 || password.length > 50) {
return ctx.fail('密码长度不正确');
}
const { pla_account } = await Framework.getModels();
// 根据手机号login_name和密码查找用户
const user = await pla_account.findOne({
where: {
login_name: phone,
pwd: password
try {
// 根据手机号查找用户使用参数化查询防止SQL注入
const user = await pla_account.findOne({
where: {
login_name: phone
}
});
if (!user) {
// 防止用户枚举攻击:统一返回相同的错误信息
return ctx.fail('手机号或密码错误');
}
});
if (!user) {
return ctx.fail('手机号或密码错误');
}
// 验证密码(支持新旧格式)
let isPasswordValid = false;
// 检查账号是否启用
if (!user.is_enabled) {
return ctx.fail('账号已被禁用');
}
if (user.pwd && user.pwd.includes('$')) {
// 新格式:加密密码
isPasswordValid = await verifyPassword(password, user.pwd);
} else {
// 旧格式:明文密码(临时兼容,建议尽快迁移)
isPasswordValid = (user.pwd === password);
// 检查授权状态
const userData = user.toJSON();
const authDate = userData.authorization_date;
const authDays = userData.authorization_days || 0;
if (authDate && authDays > 0) {
const startDate = dayjs(authDate);
const endDate = startDate.add(authDays, 'day');
const now = dayjs();
const remaining = endDate.diff(now, 'day', true);
const remaining_days = Math.max(0, Math.ceil(remaining));
if (remaining_days <= 0) {
return ctx.fail('账号授权已过期,请联系管理员续费');
// 如果验证通过,自动升级为加密密码
if (isPasswordValid) {
const { hashPassword } = require('../utils/crypto_utils');
const hashedPassword = await hashPassword(password);
await pla_account.update(
{ pwd: hashedPassword },
{ where: { id: user.id } }
);
console.log(`[安全升级] 用户 ${maskPhone(phone)} 的密码已自动加密`);
}
}
if (!isPasswordValid) {
// 记录失败尝试(用于后续添加防暴力破解功能)
console.warn(`[登录失败] 手机号: ${maskPhone(phone)}, 时间: ${new Date().toISOString()}`);
return ctx.fail('手机号或密码错误');
}
// 检查账号是否启用
if (!user.is_enabled) {
return ctx.fail('账号已被禁用,请联系管理员');
}
// 检查授权状态(拒绝过期账号登录)
const userData = user.toJSON();
const authDate = userData.authorization_date;
const authDays = userData.authorization_days || 0;
// 验证授权有效性
if (!isAuthorizationValid(authDate, authDays)) {
console.warn(`[授权过期] 用户 ${maskPhone(phone)} 尝试登录但授权已过期`);
return ctx.fail('账号授权已过期,请续费后使用');
}
// 处理设备ID
let device_id = client_device_id;
// 验证客户端提供的 device_id 格式
if (client_device_id) {
if (!validateDeviceId(client_device_id)) {
return ctx.fail('设备ID格式不正确请重新登录');
}
// 如果与数据库不同需要额外验证防止设备ID劫持
if (user.device_id && client_device_id !== user.device_id) {
// 记录设备更换
console.warn(`[设备更换] 用户 ${maskPhone(phone)} 更换设备: ${user.device_id} -> ${client_device_id}`);
// TODO: 这里可以添加更多安全检查,如:
// - 发送验证码确认
// - 记录到安全日志
// - 限制更换频率
}
// 更新设备ID
if (client_device_id !== user.device_id) {
await pla_account.update(
{ device_id: client_device_id },
{ where: { id: user.id } }
);
}
} else {
// 使用数据库中的设备ID
device_id = user.device_id;
}
// 如果仍然没有设备ID返回错误
if (!device_id) {
return ctx.fail('设备ID不能为空请重新登录');
}
// 创建token
const token = Framework.getServices().tokenService.create({
sn_code: user.sn_code,
device_id: device_id,
id: user.id
});
// 构建安全的用户信息响应(白名单模式)
const { calculateRemainingDays } = require('../utils/account_utils');
const remaining_days = calculateRemainingDays(authDate, authDays);
const safeUserInfo = {
id: user.id,
sn_code: user.sn_code,
login_name: maskPhone(user.login_name), // 脱敏处理
is_enabled: user.is_enabled,
authorization_date: user.authorization_date,
authorization_days: user.authorization_days,
remaining_days: remaining_days,
auto_deliver: user.auto_deliver,
deliver_config: user.deliver_config,
created_at: user.created_at,
updated_at: user.updated_at
};
// 记录成功登录
console.log(`[登录成功] 用户 ${maskPhone(phone)}, 剩余天数: ${remaining_days}`);
return ctx.success({
token,
device_id,
user: safeUserInfo
});
} catch (error) {
// 记录详细错误但不暴露给客户端
console.error('[登录异常]', {
error: error.message,
stack: error.stack,
phone: maskPhone(phone)
});
return ctx.fail('登录失败,请稍后重试');
}
// 处理设备ID优先使用客户端传递的 device_id如果没有则使用数据库中的
let device_id = client_device_id || user.device_id;
// 如果客户端提供了 device_id 且与数据库中的不同,则更新数据库
if (client_device_id && client_device_id !== user.device_id) {
await pla_account.update(
{ device_id: client_device_id },
{ where: { id: user.id } }
);
device_id = client_device_id;
}
// 如果既没有客户端传递的,数据库中也为空,则返回错误(不应该发生,因为客户端会生成)
if (!device_id) {
return ctx.fail('设备ID不能为空请重新登录');
}
// 创建token
const token = Framework.getServices().tokenService.create({
sn_code: user.sn_code,
device_id: device_id,
id: user.id
});
// 计算剩余天数
let remaining_days = 0;
if (authDate && authDays > 0) {
const startDate = dayjs(authDate);
const endDate = startDate.add(authDays, 'day');
const now = dayjs();
const remaining = endDate.diff(now, 'day', true);
remaining_days = Math.max(0, Math.ceil(remaining));
}
const userInfo = user.toJSON();
userInfo.remaining_days = remaining_days;
// 不返回密码
delete userInfo.pwd;
return ctx.success({
token,
device_id,
user: userInfo
});
},
/**
@@ -208,10 +279,8 @@ module.exports = {
*/
'POST /user/delivery-config/get': async (ctx) => {
try {
console.log('[User Controller] 收到获取投递配置请求');
const body = ctx.getBody();
const { sn_code } = body;
console.log('[User Controller] sn_code:', sn_code);
if (!sn_code) {
return ctx.fail('请提供设备SN码');
@@ -233,8 +302,11 @@ module.exports = {
return ctx.success({ deliver_config });
} catch (error) {
console.error('获取投递配置失败:', error);
return ctx.fail('获取投递配置失败: ' + error.message);
console.error('[获取投递配置失败]', {
error: error.message,
timestamp: new Date().toISOString()
});
return ctx.fail('获取投递配置失败');
}
},
@@ -301,10 +373,8 @@ module.exports = {
*/
'POST /user/delivery-config/save': async (ctx) => {
try {
console.log('[User Controller] 收到保存投递配置请求');
const body = ctx.getBody();
const { sn_code, deliver_config } = body;
console.log('[User Controller] sn_code:', sn_code, 'deliver_config:', JSON.stringify(deliver_config));
if (!sn_code) {
return ctx.fail('请提供设备SN码');
@@ -314,6 +384,11 @@ module.exports = {
return ctx.fail('请提供 deliver_config 配置对象');
}
// 验证 deliver_config 的基本结构
if (typeof deliver_config !== 'object') {
return ctx.fail('deliver_config 必须是对象');
}
const { pla_account } = await Framework.getModels();
// 根据 sn_code 查找账号
@@ -334,10 +409,19 @@ module.exports = {
{ where: { id: user.id } }
);
console.log('[保存投递配置成功]', {
sn_code,
auto_delivery: deliver_config.auto_delivery,
timestamp: new Date().toISOString()
});
return ctx.success({ message: '配置保存成功' });
} catch (error) {
console.error('保存投递配置失败:', error);
return ctx.fail('保存投递配置失败: ' + error.message);
console.error('[保存投递配置失败]', {
error: error.message,
timestamp: new Date().toISOString()
});
return ctx.fail('保存投递配置失败');
}
}
}