1
This commit is contained in:
@@ -6,7 +6,7 @@ module.exports = {
|
||||
* /api/user/login:
|
||||
* post:
|
||||
* summary: 用户登录
|
||||
* description: 通过手机号和密码登录,返回token、device_id和用户信息
|
||||
* description: 通过邮箱和密码登录,返回token、device_id和用户信息
|
||||
* tags: [前端-用户管理]
|
||||
* requestBody:
|
||||
* required: true
|
||||
@@ -15,13 +15,13 @@ module.exports = {
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - phone
|
||||
* - email
|
||||
* - password
|
||||
* properties:
|
||||
* phone:
|
||||
* email:
|
||||
* type: string
|
||||
* description: 手机号(登录名)
|
||||
* example: '13800138000'
|
||||
* description: 邮箱(登录名)
|
||||
* example: 'user@example.com'
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
@@ -75,20 +75,22 @@ module.exports = {
|
||||
* example: '用户不存在或密码错误'
|
||||
*/
|
||||
"POST /user/login": async (ctx) => {
|
||||
const { phone, password, device_id: client_device_id } = ctx.getBody();
|
||||
const { email, 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');
|
||||
const { verifyPassword, validateDeviceId, maskEmail } = require('../utils/crypto_utils');
|
||||
|
||||
// 参数验证
|
||||
if (!phone || !password) {
|
||||
return ctx.fail('手机号和密码不能为空');
|
||||
if (!email || !password) {
|
||||
return ctx.fail('邮箱和密码不能为空');
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phonePattern = /^1[3-9]\d{9}$/;
|
||||
if (!phonePattern.test(phone)) {
|
||||
return ctx.fail('手机号格式不正确');
|
||||
// 统一邮箱地址为小写
|
||||
const email_normalized = email.toLowerCase().trim();
|
||||
|
||||
// 验证邮箱格式
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email_normalized)) {
|
||||
return ctx.fail('邮箱格式不正确');
|
||||
}
|
||||
|
||||
// 验证密码长度
|
||||
@@ -99,16 +101,16 @@ module.exports = {
|
||||
const { pla_account } = await Framework.getModels();
|
||||
|
||||
try {
|
||||
// 根据手机号查找用户(使用参数化查询防止SQL注入)
|
||||
// 根据邮箱查找用户(使用统一的小写邮箱)
|
||||
const user = await pla_account.findOne({
|
||||
where: {
|
||||
login_name: phone
|
||||
login_name: email_normalized
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
// 防止用户枚举攻击:统一返回相同的错误信息
|
||||
return ctx.fail('手机号或密码错误');
|
||||
return ctx.fail('邮箱或密码错误');
|
||||
}
|
||||
|
||||
// 验证密码(支持新旧格式)
|
||||
@@ -129,14 +131,14 @@ module.exports = {
|
||||
{ pwd: hashedPassword },
|
||||
{ where: { id: user.id } }
|
||||
);
|
||||
console.log(`[安全升级] 用户 ${maskPhone(phone)} 的密码已自动加密`);
|
||||
console.log(`[安全升级] 用户 ${maskEmail(email_normalized)} 的密码已自动加密`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPasswordValid) {
|
||||
// 记录失败尝试(用于后续添加防暴力破解功能)
|
||||
console.warn(`[登录失败] 手机号: ${maskPhone(phone)}, 时间: ${new Date().toISOString()}`);
|
||||
return ctx.fail('手机号或密码错误');
|
||||
console.warn(`[登录失败] 邮箱: ${maskEmail(email_normalized)}, 时间: ${new Date().toISOString()}`);
|
||||
return ctx.fail('邮箱或密码错误');
|
||||
}
|
||||
|
||||
// 检查账号是否启用
|
||||
@@ -144,16 +146,14 @@ module.exports = {
|
||||
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('账号授权已过期,请续费后使用');
|
||||
}
|
||||
// 使用工具函数计算剩余天数
|
||||
const { calculateRemainingDays } = require('../utils/account_utils');
|
||||
const remaining_days = calculateRemainingDays(authDate, authDays);
|
||||
|
||||
// 处理设备ID
|
||||
let device_id = client_device_id;
|
||||
@@ -167,7 +167,7 @@ module.exports = {
|
||||
// 如果与数据库不同,需要额外验证(防止设备ID劫持)
|
||||
if (user.device_id && client_device_id !== user.device_id) {
|
||||
// 记录设备更换
|
||||
console.warn(`[设备更换] 用户 ${maskPhone(phone)} 更换设备: ${user.device_id} -> ${client_device_id}`);
|
||||
console.warn(`[设备更换] 用户 ${maskEmail(email_normalized)} 更换设备: ${user.device_id} -> ${client_device_id}`);
|
||||
|
||||
// TODO: 这里可以添加更多安全检查,如:
|
||||
// - 发送验证码确认
|
||||
@@ -200,13 +200,10 @@ module.exports = {
|
||||
});
|
||||
|
||||
// 构建安全的用户信息响应(白名单模式)
|
||||
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), // 脱敏处理
|
||||
login_name: maskEmail(user.login_name), // 脱敏处理
|
||||
is_enabled: user.is_enabled,
|
||||
authorization_date: user.authorization_date,
|
||||
authorization_days: user.authorization_days,
|
||||
@@ -218,7 +215,7 @@ module.exports = {
|
||||
};
|
||||
|
||||
// 记录成功登录
|
||||
console.log(`[登录成功] 用户 ${maskPhone(phone)}, 剩余天数: ${remaining_days}`);
|
||||
console.log(`[登录成功] 用户 ${maskEmail(email_normalized)}, 剩余天数: ${remaining_days}`);
|
||||
|
||||
return ctx.success({
|
||||
token,
|
||||
@@ -230,7 +227,7 @@ module.exports = {
|
||||
console.error('[登录异常]', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
phone: maskPhone(phone)
|
||||
email: maskEmail(email_normalized)
|
||||
});
|
||||
return ctx.fail('登录失败,请稍后重试');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user