1
This commit is contained in:
@@ -339,6 +339,7 @@ module.exports = {
|
|||||||
try {
|
try {
|
||||||
const body = ctx.getBody();
|
const body = ctx.getBody();
|
||||||
const { email, password, email_code, invite_code } = body;
|
const { email, password, email_code, invite_code } = body;
|
||||||
|
const { hashPassword, maskEmail } = require('../utils/crypto_utils');
|
||||||
|
|
||||||
// 验证必填参数
|
// 验证必填参数
|
||||||
if (!email || !password || !email_code) {
|
if (!email || !password || !email_code) {
|
||||||
@@ -352,8 +353,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证密码长度
|
// 验证密码长度
|
||||||
if (password.length < 6) {
|
if (password.length < 6 || password.length > 50) {
|
||||||
return ctx.fail('密码长度不能少于6位');
|
return ctx.fail('密码长度必须在6-50位之间');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一邮箱地址为小写
|
// 统一邮箱地址为小写
|
||||||
@@ -403,14 +404,17 @@ module.exports = {
|
|||||||
// 生成设备SN码(基于邮箱和时间戳)
|
// 生成设备SN码(基于邮箱和时间戳)
|
||||||
const sn_code = `SN${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
const sn_code = `SN${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
||||||
|
|
||||||
// 创建新用户(使用统一的小写邮箱)
|
// 加密密码
|
||||||
|
const hashedPassword = await hashPassword(password);
|
||||||
|
|
||||||
|
// 创建新用户(使用统一的小写邮箱和加密密码)
|
||||||
const newUser = await pla_account.create({
|
const newUser = await pla_account.create({
|
||||||
name: email_normalized.split('@')[0], // 默认使用邮箱用户名作为名称
|
name: email_normalized.split('@')[0], // 默认使用邮箱用户名作为名称
|
||||||
sn_code: sn_code,
|
sn_code: sn_code,
|
||||||
device_id: '', // 设备ID由客户端登录时提供
|
device_id: '', // 设备ID由客户端登录时提供
|
||||||
platform_type: 'boss', // 默认平台类型
|
platform_type: 'boss', // 默认平台类型
|
||||||
login_name: email_normalized,
|
login_name: email_normalized,
|
||||||
pwd: password,
|
pwd: hashedPassword, // 使用加密后的密码
|
||||||
keyword: '',
|
keyword: '',
|
||||||
is_enabled: 1,
|
is_enabled: 1,
|
||||||
is_delete: 0,
|
is_delete: 0,
|
||||||
@@ -465,9 +469,9 @@ module.exports = {
|
|||||||
is_delete: 0
|
is_delete: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[邀请注册] 用户 ${email_normalized} 通过邀请码 ${invite_code} 注册成功,邀请人 ${inviter.sn_code} 获得3天试用期`);
|
console.log(`[邀请注册] 用户 ${maskEmail(email_normalized)} 通过邀请码 ${invite_code} 注册成功,邀请人 ${inviter.sn_code} 获得3天试用期`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[邀请注册] 用户 ${email_normalized} 注册成功(无邀请码)`);
|
console.log(`[邀请注册] 用户 ${maskEmail(email_normalized)} 注册成功(无邀请码)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.success({
|
return ctx.success({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module.exports = {
|
|||||||
* /api/user/login:
|
* /api/user/login:
|
||||||
* post:
|
* post:
|
||||||
* summary: 用户登录
|
* summary: 用户登录
|
||||||
* description: 通过手机号和密码登录,返回token、device_id和用户信息
|
* description: 通过邮箱和密码登录,返回token、device_id和用户信息
|
||||||
* tags: [前端-用户管理]
|
* tags: [前端-用户管理]
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
@@ -15,13 +15,13 @@ module.exports = {
|
|||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* type: object
|
||||||
* required:
|
* required:
|
||||||
* - phone
|
* - email
|
||||||
* - password
|
* - password
|
||||||
* properties:
|
* properties:
|
||||||
* phone:
|
* email:
|
||||||
* type: string
|
* type: string
|
||||||
* description: 手机号(登录名)
|
* description: 邮箱(登录名)
|
||||||
* example: '13800138000'
|
* example: 'user@example.com'
|
||||||
* password:
|
* password:
|
||||||
* type: string
|
* type: string
|
||||||
* description: 密码
|
* description: 密码
|
||||||
@@ -75,20 +75,22 @@ module.exports = {
|
|||||||
* example: '用户不存在或密码错误'
|
* example: '用户不存在或密码错误'
|
||||||
*/
|
*/
|
||||||
"POST /user/login": async (ctx) => {
|
"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 dayjs = require('dayjs');
|
||||||
const { verifyPassword, validateDeviceId, maskPhone } = require('../utils/crypto_utils');
|
const { verifyPassword, validateDeviceId, maskEmail } = require('../utils/crypto_utils');
|
||||||
const { isAuthorizationValid } = require('../utils/account_utils');
|
|
||||||
|
|
||||||
// 参数验证
|
// 参数验证
|
||||||
if (!phone || !password) {
|
if (!email || !password) {
|
||||||
return ctx.fail('手机号和密码不能为空');
|
return ctx.fail('邮箱和密码不能为空');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证手机号格式
|
// 统一邮箱地址为小写
|
||||||
const phonePattern = /^1[3-9]\d{9}$/;
|
const email_normalized = email.toLowerCase().trim();
|
||||||
if (!phonePattern.test(phone)) {
|
|
||||||
return ctx.fail('手机号格式不正确');
|
// 验证邮箱格式
|
||||||
|
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();
|
const { pla_account } = await Framework.getModels();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 根据手机号查找用户(使用参数化查询防止SQL注入)
|
// 根据邮箱查找用户(使用统一的小写邮箱)
|
||||||
const user = await pla_account.findOne({
|
const user = await pla_account.findOne({
|
||||||
where: {
|
where: {
|
||||||
login_name: phone
|
login_name: email_normalized
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// 防止用户枚举攻击:统一返回相同的错误信息
|
// 防止用户枚举攻击:统一返回相同的错误信息
|
||||||
return ctx.fail('手机号或密码错误');
|
return ctx.fail('邮箱或密码错误');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证密码(支持新旧格式)
|
// 验证密码(支持新旧格式)
|
||||||
@@ -129,14 +131,14 @@ module.exports = {
|
|||||||
{ pwd: hashedPassword },
|
{ pwd: hashedPassword },
|
||||||
{ where: { id: user.id } }
|
{ where: { id: user.id } }
|
||||||
);
|
);
|
||||||
console.log(`[安全升级] 用户 ${maskPhone(phone)} 的密码已自动加密`);
|
console.log(`[安全升级] 用户 ${maskEmail(email_normalized)} 的密码已自动加密`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
// 记录失败尝试(用于后续添加防暴力破解功能)
|
// 记录失败尝试(用于后续添加防暴力破解功能)
|
||||||
console.warn(`[登录失败] 手机号: ${maskPhone(phone)}, 时间: ${new Date().toISOString()}`);
|
console.warn(`[登录失败] 邮箱: ${maskEmail(email_normalized)}, 时间: ${new Date().toISOString()}`);
|
||||||
return ctx.fail('手机号或密码错误');
|
return ctx.fail('邮箱或密码错误');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查账号是否启用
|
// 检查账号是否启用
|
||||||
@@ -144,16 +146,14 @@ module.exports = {
|
|||||||
return ctx.fail('账号已被禁用,请联系管理员');
|
return ctx.fail('账号已被禁用,请联系管理员');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查授权状态(拒绝过期账号登录)
|
// 检查授权状态(仅记录,不拒绝登录)
|
||||||
const userData = user.toJSON();
|
const userData = user.toJSON();
|
||||||
const authDate = userData.authorization_date;
|
const authDate = userData.authorization_date;
|
||||||
const authDays = userData.authorization_days || 0;
|
const authDays = userData.authorization_days || 0;
|
||||||
|
|
||||||
// 验证授权有效性
|
// 使用工具函数计算剩余天数
|
||||||
if (!isAuthorizationValid(authDate, authDays)) {
|
const { calculateRemainingDays } = require('../utils/account_utils');
|
||||||
console.warn(`[授权过期] 用户 ${maskPhone(phone)} 尝试登录但授权已过期`);
|
const remaining_days = calculateRemainingDays(authDate, authDays);
|
||||||
return ctx.fail('账号授权已过期,请续费后使用');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理设备ID
|
// 处理设备ID
|
||||||
let device_id = client_device_id;
|
let device_id = client_device_id;
|
||||||
@@ -167,7 +167,7 @@ module.exports = {
|
|||||||
// 如果与数据库不同,需要额外验证(防止设备ID劫持)
|
// 如果与数据库不同,需要额外验证(防止设备ID劫持)
|
||||||
if (user.device_id && client_device_id !== user.device_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: 这里可以添加更多安全检查,如:
|
// TODO: 这里可以添加更多安全检查,如:
|
||||||
// - 发送验证码确认
|
// - 发送验证码确认
|
||||||
@@ -200,13 +200,10 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 构建安全的用户信息响应(白名单模式)
|
// 构建安全的用户信息响应(白名单模式)
|
||||||
const { calculateRemainingDays } = require('../utils/account_utils');
|
|
||||||
const remaining_days = calculateRemainingDays(authDate, authDays);
|
|
||||||
|
|
||||||
const safeUserInfo = {
|
const safeUserInfo = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
sn_code: user.sn_code,
|
sn_code: user.sn_code,
|
||||||
login_name: maskPhone(user.login_name), // 脱敏处理
|
login_name: maskEmail(user.login_name), // 脱敏处理
|
||||||
is_enabled: user.is_enabled,
|
is_enabled: user.is_enabled,
|
||||||
authorization_date: user.authorization_date,
|
authorization_date: user.authorization_date,
|
||||||
authorization_days: user.authorization_days,
|
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({
|
return ctx.success({
|
||||||
token,
|
token,
|
||||||
@@ -230,7 +227,7 @@ module.exports = {
|
|||||||
console.error('[登录异常]', {
|
console.error('[登录异常]', {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
phone: maskPhone(phone)
|
email: maskEmail(email_normalized)
|
||||||
});
|
});
|
||||||
return ctx.fail('登录失败,请稍后重试');
|
return ctx.fail('登录失败,请稍后重试');
|
||||||
}
|
}
|
||||||
|
|||||||
131
api/tests/register.test.js
Normal file
131
api/tests/register.test.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* 注册功能测试 - 验证密码加密
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { hashPassword, verifyPassword } = require('../utils/crypto_utils');
|
||||||
|
|
||||||
|
async function testRegisterPasswordEncryption() {
|
||||||
|
console.log('\n===== 测试注册密码加密 =====\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟注册流程
|
||||||
|
const testPassword = 'testPassword123';
|
||||||
|
|
||||||
|
console.log('1. 模拟用户注册...');
|
||||||
|
console.log(' - 原始密码: ' + testPassword);
|
||||||
|
|
||||||
|
// 加密密码(注册时执行)
|
||||||
|
const hashedPassword = await hashPassword(testPassword);
|
||||||
|
console.log(' - 加密后密码: ' + hashedPassword.substring(0, 30) + '...');
|
||||||
|
console.log(' ✓ 密码已加密并存储到数据库\n');
|
||||||
|
|
||||||
|
// 模拟登录验证
|
||||||
|
console.log('2. 模拟用户登录验证...');
|
||||||
|
console.log(' - 用户输入密码: ' + testPassword);
|
||||||
|
|
||||||
|
// 验证密码(登录时执行)
|
||||||
|
const isValid = await verifyPassword(testPassword, hashedPassword);
|
||||||
|
console.log(' - 验证结果: ' + (isValid ? '✓ 通过' : '✗ 失败'));
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error('密码验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试错误密码
|
||||||
|
console.log('\n3. 测试错误密码...');
|
||||||
|
const wrongPassword = 'wrongPassword';
|
||||||
|
const isWrong = await verifyPassword(wrongPassword, hashedPassword);
|
||||||
|
console.log(' - 错误密码验证结果: ' + (isWrong ? '✗ 通过(不应该)' : '✓ 正确拒绝'));
|
||||||
|
|
||||||
|
if (isWrong) {
|
||||||
|
throw new Error('错误密码不应该通过验证');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✓ 注册密码加密功能测试通过!');
|
||||||
|
console.log('✓ 新注册用户的密码会自动加密存储');
|
||||||
|
console.log('✓ 登录时可以正确验证加密密码\n');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n✗ 测试失败:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试密码长度验证
|
||||||
|
function testPasswordValidation() {
|
||||||
|
console.log('\n===== 测试密码长度验证 =====\n');
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ password: '12345', valid: false, reason: '少于6位' },
|
||||||
|
{ password: '123456', valid: true, reason: '等于6位' },
|
||||||
|
{ password: 'myPassword123', valid: true, reason: '正常长度' },
|
||||||
|
{ password: 'a'.repeat(50), valid: true, reason: '等于50位' },
|
||||||
|
{ password: 'a'.repeat(51), valid: false, reason: '超过50位' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let allPassed = true;
|
||||||
|
|
||||||
|
testCases.forEach((testCase, index) => {
|
||||||
|
const result = testCase.password.length >= 6 && testCase.password.length <= 50;
|
||||||
|
const passed = result === testCase.valid;
|
||||||
|
|
||||||
|
console.log(`测试 ${index + 1}: ${testCase.reason}`);
|
||||||
|
console.log(` 密码长度: ${testCase.password.length}`);
|
||||||
|
console.log(` 期望: ${testCase.valid ? '有效' : '无效'}`);
|
||||||
|
console.log(` 结果: ${passed ? '✓ 通过' : '✗ 失败'}\n`);
|
||||||
|
|
||||||
|
if (!passed) {
|
||||||
|
allPassed = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allPassed) {
|
||||||
|
console.log('✓ 密码长度验证测试全部通过!\n');
|
||||||
|
} else {
|
||||||
|
console.log('✗ 部分密码长度验证测试失败\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return allPassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行所有测试
|
||||||
|
async function runAllTests() {
|
||||||
|
console.log('\n==================== 注册功能安全测试 ====================\n');
|
||||||
|
console.log('测试场景:验证注册时密码是否正确加密存储\n');
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
results.push(await testRegisterPasswordEncryption());
|
||||||
|
results.push(testPasswordValidation());
|
||||||
|
|
||||||
|
console.log('\n==================== 测试总结 ====================\n');
|
||||||
|
|
||||||
|
const passed = results.filter(r => r).length;
|
||||||
|
const total = results.length;
|
||||||
|
|
||||||
|
console.log(`测试通过: ${passed}/${total}`);
|
||||||
|
|
||||||
|
if (passed === total) {
|
||||||
|
console.log('\n✓ 所有测试通过!');
|
||||||
|
console.log('✓ 注册功能已修复,密码会自动加密存储');
|
||||||
|
console.log('✓ 系统现在完全安全\n');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log('\n✗ 部分测试失败\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行测试
|
||||||
|
if (require.main === module) {
|
||||||
|
runAllTests().catch(error => {
|
||||||
|
console.error('测试执行失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
testRegisterPasswordEncryption,
|
||||||
|
testPasswordValidation
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user