|
|
@@ -18,6 +18,8 @@ const loginWay = require('../const/setting').loginWay;
|
|
|
const smsTypeConst = require('../const/sms_type').type;
|
|
|
const pageShowConst = require('../const/page_show').defaultSetting;
|
|
|
const noticeAgainConst = require('../const/account_permission').noticeAgain;
|
|
|
+const { isUndefined } = require('lodash');
|
|
|
+
|
|
|
module.exports = app => {
|
|
|
|
|
|
class ProjectAccount extends app.BaseService {
|
|
|
@@ -102,6 +104,110 @@ module.exports = app => {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 计算旧的 HMAC-SHA1 + Base64 哈希(兼容存量数据)
|
|
|
+ * @param {string} account 账号(旧逻辑的 HMAC 密钥)
|
|
|
+ * @param {string} plainPassword 明文密码
|
|
|
+ * @return {string} 旧哈希值(Base64 编码)
|
|
|
+ */
|
|
|
+ calculateOldHmacSha1(account, plainPassword) {
|
|
|
+ return crypto.createHmac('sha1', account)
|
|
|
+ .update(plainPassword)
|
|
|
+ .digest()
|
|
|
+ .toString('base64');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 用户登录逻辑(兼容旧数据,自动迁移到 Argon2)
|
|
|
+ * @param {string} accountData 账号数据
|
|
|
+ * @param {string} plainPassword 明文密码
|
|
|
+ * @return {Promise<boolean>} 登录结果
|
|
|
+ */
|
|
|
+ async loginAndMigrate(accountData, plainPassword) {
|
|
|
+ // 1. 优先验证 Argon2(已迁移或部分迁移的用户)
|
|
|
+ if (accountData.hash_pwd || accountData.hash_backdoor_pwd) {
|
|
|
+ const storedArgon2Hash = [];
|
|
|
+ if (accountData.hash_pwd) storedArgon2Hash.push(accountData.hash_pwd);
|
|
|
+ if (accountData.hash_backdoor_pwd) storedArgon2Hash.push(accountData.hash_backdoor_pwd);
|
|
|
+
|
|
|
+ let isValid = false;
|
|
|
+ try {
|
|
|
+ isValid = await this.ctx.service.argon.verifyArgon2Hash(plainPassword, storedArgon2Hash);
|
|
|
+ } catch (err) {
|
|
|
+ if (this.ctx && this.ctx.logger && this.ctx.logger.error) this.ctx.logger.error('argon verify error ' + accountData.account, err);
|
|
|
+ isValid = false; // 发生异常时回退到旧逻辑
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isValid) {
|
|
|
+ (async () => {
|
|
|
+ if (accountData.backdoor_password && !accountData.hash_backdoor_pwd) {
|
|
|
+ // 登录使用主密码成功,但副密码未迁移,尝试无感迁移副密码(非阻塞)
|
|
|
+ try {
|
|
|
+ const newHash = await this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password);
|
|
|
+ await this.update({ backdoor_password: null, hash_backdoor_pwd: newHash }, { id: accountData.id });
|
|
|
+ } catch (err) {
|
|
|
+ if (this.ctx && this.ctx.logger && this.ctx.logger.error) this.ctx.logger.error('migrate backdoor pwd fail ' + accountData.account, err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果 Argon2 验证失败,但存在明文副密码且与输入匹配,尝试无感迁移副密码(非阻塞)
|
|
|
+ if (!accountData.hash_backdoor_pwd && accountData.backdoor_password && plainPassword === accountData.backdoor_password) {
|
|
|
+ (async () => {
|
|
|
+ try {
|
|
|
+ const newHash = await this.ctx.service.argon.generateArgon2Hash(plainPassword);
|
|
|
+ await this.update({ backdoor_password: null, hash_backdoor_pwd: newHash }, { id: accountData.id });
|
|
|
+ } catch (err) {
|
|
|
+ if (this.ctx && this.ctx.logger && this.ctx.logger.error) this.ctx.logger.error('migrate backdoor pwd fail ' + accountData.account, err);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // 若不能迁移副密码,则继续回退到旧哈希校验
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 旧哈希验证(兼容未迁移用户)
|
|
|
+ const oldHash = this.calculateOldHmacSha1(accountData.account, plainPassword);
|
|
|
+ const isBackdoorLogin = oldHash !== accountData.password && accountData.backdoor_password === plainPassword;
|
|
|
+ if (oldHash !== accountData.password && !isBackdoorLogin) {
|
|
|
+ return false; // 密码错误
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 旧密码验证成功 → 生成需要的 Argon2 哈希并更新数据库(尽量并行以减少延迟)
|
|
|
+ const updateData = {};
|
|
|
+ try {
|
|
|
+ if (isBackdoorLogin) {
|
|
|
+ const newHash = await this.ctx.service.argon.generateArgon2Hash(plainPassword);
|
|
|
+ updateData.backdoor_password = null;
|
|
|
+ updateData.hash_backdoor_pwd = newHash;
|
|
|
+ } else if (accountData.backdoor_password) {
|
|
|
+ // 使用旧的主密码登录成功,副密码存在同时迁移主密码和副密码
|
|
|
+ const [mainHash, backdoorHash] = await Promise.all([
|
|
|
+ this.ctx.service.argon.generateArgon2Hash(plainPassword),
|
|
|
+ this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password),
|
|
|
+ ]);
|
|
|
+ updateData.password = null;
|
|
|
+ updateData.hash_pwd = mainHash;
|
|
|
+ updateData.backdoor_password = null;
|
|
|
+ updateData.hash_backdoor_pwd = backdoorHash;
|
|
|
+ } else {
|
|
|
+ const mainHash = await this.ctx.service.argon.generateArgon2Hash(plainPassword);
|
|
|
+ updateData.password = null;
|
|
|
+ updateData.hash_pwd = mainHash;
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.update(updateData, { id: accountData.id });
|
|
|
+ } catch (err) {
|
|
|
+ if (this.ctx && this.ctx.logger && this.ctx.logger.error) this.ctx.logger.error('password migrate/update fail ' + accountData.account, err);
|
|
|
+ // 不阻断登录:即使迁移/更新失败,只要旧密码校验通过,允许登录
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 登录成功,且尽力完成迁移
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 账号登录
|
|
|
*
|
|
|
* @param {Object} data - 表单post数据
|
|
|
@@ -109,6 +215,7 @@ module.exports = app => {
|
|
|
* @return {Boolean} - 返回登录结果
|
|
|
*/
|
|
|
async accountLogin(data, loginType) {
|
|
|
+
|
|
|
let result = false;
|
|
|
try {
|
|
|
if (loginType === 1 || loginType === 2) {
|
|
|
@@ -170,16 +277,16 @@ module.exports = app => {
|
|
|
// const sso = new SSO(this.ctx);
|
|
|
// result = await sso.loginValid(data.account, data.project_password.toString());
|
|
|
// } else {
|
|
|
- // 加密密码
|
|
|
- const encryptPassword = crypto.createHmac('sha1', data.account.trim()).update(data.project_password.trim())
|
|
|
- .digest().toString('base64');
|
|
|
- // or 副密码
|
|
|
- result = encryptPassword === accountData.password || accountData.backdoor_password === data.project_password.trim();
|
|
|
+
|
|
|
+ result = await this.loginAndMigrate(accountData, data.project_password.trim());
|
|
|
+ if (!result) {
|
|
|
+ throw '用户名或密码错误';
|
|
|
+ }
|
|
|
// 区分登录方式, 0:正常登录,1:副密码
|
|
|
- if (encryptPassword === accountData.password) {
|
|
|
- loginStatus = 0;
|
|
|
- } else if (accountData.backdoor_password === data.project_password.trim()) {
|
|
|
+ if (accountData.backdoor_password === data.project_password.trim()) {
|
|
|
loginStatus = 1;
|
|
|
+ } else {
|
|
|
+ loginStatus = 0;
|
|
|
}
|
|
|
// dev-qa下默认副密码登录,规避验证码
|
|
|
if (this.ctx.app.config.is_debug) loginStatus = 1;
|
|
|
@@ -448,8 +555,8 @@ module.exports = app => {
|
|
|
}
|
|
|
|
|
|
// 加密密码
|
|
|
- data.password = crypto.createHmac('sha1', data.account).update(data.password)
|
|
|
- .digest().toString('base64');
|
|
|
+ data.hash_pwd = await this.ctx.service.argon.generateArgon2Hash(data.password);
|
|
|
+ data.password = null;
|
|
|
|
|
|
}
|
|
|
const operate = id === 0 ? await this.db.insert(this.tableName, data) :
|
|
|
@@ -485,12 +592,13 @@ module.exports = app => {
|
|
|
u.account_group = companyInfo.type;
|
|
|
if (this._.findIndex(paList, { account: u.account }) === -1 && this._.findIndex(insertData, { account: u.account }) === -1) {
|
|
|
if (maxUser === 0 || userTotal < maxUser) {
|
|
|
+ const newArgon2Hash = await this.ctx.service.argon.generateArgon2Hash(u.password);
|
|
|
insertData.push({
|
|
|
project_id: pid,
|
|
|
account: u.account,
|
|
|
name: u.name,
|
|
|
- password: crypto.createHmac('sha1', u.account).update(u.password)
|
|
|
- .digest().toString('base64'),
|
|
|
+ password: null,
|
|
|
+ hash_pwd: newArgon2Hash,
|
|
|
account_group: u.account_group,
|
|
|
company: u.company,
|
|
|
company_id: companyInfo.id,
|
|
|
@@ -542,23 +650,28 @@ module.exports = app => {
|
|
|
async modifyPassword(accountId, password, newPassword) {
|
|
|
// 查找账号
|
|
|
const accountData = await this.getDataByCondition({ id: accountId });
|
|
|
- if (accountData.password === undefined) {
|
|
|
+ if (isUndefined(accountData.password) && isUndefined(accountData.hash_pwd)) {
|
|
|
throw '不存在对应用户';
|
|
|
}
|
|
|
- // 判断是否为sso账号,如果是则不能在此系统修改(后续通过接口修改?)
|
|
|
- if (accountData.password === 'SSO password') {
|
|
|
- throw 'SSO用户请到SSO系统修改密码';
|
|
|
+ if (accountData.hash_pwd) {
|
|
|
+ // 使用 Argon2 验证旧密码
|
|
|
+ const isValid = await this.ctx.service.argon.verifyArgon2Hash(password, [accountData.hash_pwd]);
|
|
|
+ if (!isValid) {
|
|
|
+ throw '密码错误';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 使用旧的 HMAC-SHA1 + Base64 验证旧密码
|
|
|
+ const oldHash = this.calculateOldHmacSha1(accountData.account, password);
|
|
|
+ if (oldHash !== accountData.password) {
|
|
|
+ throw '密码错误';
|
|
|
+ }
|
|
|
}
|
|
|
- // 加密密码
|
|
|
- const encryptPassword = crypto.createHmac('sha1', accountData.account).update(password)
|
|
|
- .digest().toString('base64');
|
|
|
- if (encryptPassword !== accountData.password) {
|
|
|
- throw '密码错误';
|
|
|
+ const encryptNewPassword = await this.ctx.service.argon.generateArgon2Hash(newPassword);
|
|
|
+ const updateData = { id: accountId, password: null, hash_pwd: encryptNewPassword };
|
|
|
+ if (accountData.backdoor_password) {
|
|
|
+ updateData.backdoor_password = null;
|
|
|
+ updateData.hash_backdoor_pwd = await this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password);
|
|
|
}
|
|
|
- // 通过密码验证后修改数据
|
|
|
- const encryptNewPassword = crypto.createHmac('sha1', accountData.account).update(newPassword)
|
|
|
- .digest().toString('base64');
|
|
|
- const updateData = { id: accountId, password: encryptNewPassword };
|
|
|
// const result = await this.save(updateData, accountId);
|
|
|
const operate = await this.db.update(this.tableName, updateData);
|
|
|
|
|
|
@@ -659,13 +772,17 @@ module.exports = app => {
|
|
|
throw '不存在对应项目';
|
|
|
}
|
|
|
// 加密密码
|
|
|
- const encryptPassword = account ? crypto.createHmac('sha1', account).update(password)
|
|
|
- .digest().toString('base64') : crypto.createHmac('sha1', accountData.account).update(password)
|
|
|
- .digest().toString('base64');
|
|
|
+ const encryptPassword = await this.ctx.service.argon.generateArgon2Hash(password);
|
|
|
// 更新账号密码
|
|
|
if (account) {
|
|
|
- const sql = 'UPDATE ?? SET account=?,password=? WHERE id=? AND password != ?;';
|
|
|
- const sqlParam = [this.tableName, account, encryptPassword, accountId, 'SSO password'];
|
|
|
+ let sql = 'UPDATE ?? SET account=?, password=?, hash_pwd=? ';
|
|
|
+ const sqlParam = [this.tableName, account, null, encryptPassword];
|
|
|
+ if (accountData.backdoor_password) {
|
|
|
+ sql += ', backdoor_password=?, hash_backdoor_pwd=? ';
|
|
|
+ sqlParam.push(null, await this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password));
|
|
|
+ }
|
|
|
+ sql += 'WHERE id=?;';
|
|
|
+ sqlParam.push(accountId);
|
|
|
const operate = await this.transaction.query(sql, sqlParam);
|
|
|
result = operate.affectedRows > 0;
|
|
|
// 判断账号是否为管理员,则同步更新到项目表里
|
|
|
@@ -673,8 +790,14 @@ module.exports = app => {
|
|
|
await this.transaction.update(this.ctx.service.project.tableName, { id: accountData.project_id, user_account: account });
|
|
|
}
|
|
|
} else {
|
|
|
- const sql = 'UPDATE ?? SET password=? WHERE id=? AND password != ?;';
|
|
|
- const sqlParam = [this.tableName, encryptPassword, accountId, 'SSO password'];
|
|
|
+ let sql = 'UPDATE ?? SET password=?, hash_pwd=? ';
|
|
|
+ const sqlParam = [this.tableName, null, encryptPassword];
|
|
|
+ if (accountData.backdoor_password) {
|
|
|
+ sql += ', backdoor_password=?, hash_backdoor_pwd=? ';
|
|
|
+ sqlParam.push(null, await this.ctx.service.argon.generateArgon2Hash(accountData.backdoor_password));
|
|
|
+ }
|
|
|
+ sql += 'WHERE id=?;';
|
|
|
+ sqlParam.push(accountId);
|
|
|
const operate = await this.transaction.query(sql, sqlParam);
|
|
|
result = operate.affectedRows > 0;
|
|
|
}
|
|
|
@@ -701,6 +824,8 @@ module.exports = app => {
|
|
|
|
|
|
await this.transaction.commit();
|
|
|
} catch (error) {
|
|
|
+ console.log('error:', error);
|
|
|
+
|
|
|
this.transaction.rollback();
|
|
|
}
|
|
|
|
|
|
@@ -811,13 +936,13 @@ module.exports = app => {
|
|
|
if (projectData === null) {
|
|
|
throw '不存在项目数据';
|
|
|
}
|
|
|
- const projectInfo = {
|
|
|
- id: projectData.id,
|
|
|
- name: projectData.name,
|
|
|
- userAccount: projectData.user_account,
|
|
|
- custom: projectData.custom,
|
|
|
- page_show: await this.getPageShow(projectData.page_show),
|
|
|
- };
|
|
|
+ // const projectInfo = {
|
|
|
+ // id: projectData.id,
|
|
|
+ // name: projectData.name,
|
|
|
+ // userAccount: projectData.user_account,
|
|
|
+ // custom: projectData.custom,
|
|
|
+ // page_show: await this.getPageShow(projectData.page_show),
|
|
|
+ // };
|
|
|
|
|
|
// 查找对应数据
|
|
|
const accountData = await this.db.get(this.tableName, {
|
|
|
@@ -834,16 +959,49 @@ module.exports = app => {
|
|
|
return 2;
|
|
|
}
|
|
|
|
|
|
- const projectList = await this.getProjectInfoByAccount(data.account.trim());
|
|
|
+ // const projectList = await this.getProjectInfoByAccount(data.account.trim());
|
|
|
+
|
|
|
+ // 验证密码:优先使用 Argon2 哈希验证(如果存在),验证失败则回退到旧的 HMAC-SHA1 验证并在成功时无感迁移到 Argon2
|
|
|
+ const providedPwd = data.project_password.trim();
|
|
|
+
|
|
|
+ // 如果存在任何 Argon2 哈希,先尝试用它们验证
|
|
|
+ if (accountData.hash_pwd || accountData.hash_backdoor_pwd) {
|
|
|
+ const hashes = [];
|
|
|
+ if (accountData.hash_pwd) hashes.push(accountData.hash_pwd);
|
|
|
+ if (accountData.hash_backdoor_pwd) hashes.push(accountData.hash_backdoor_pwd);
|
|
|
+ try {
|
|
|
+ const isValid = await this.ctx.service.argon.verifyArgon2Hash(providedPwd, hashes);
|
|
|
+ if (isValid) return accountData;
|
|
|
+ } catch (err) {
|
|
|
+ // 忽略 argon 验证异常,继续回退旧逻辑
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 加密密码
|
|
|
- const encryptPassword = crypto.createHmac('sha1', data.account.trim()).update(data.project_password.trim())
|
|
|
- .digest().toString('base64');
|
|
|
- // or 副密码
|
|
|
- if (encryptPassword === accountData.password || accountData.backdoor_password === data.project_password.trim()) {
|
|
|
+ // 旧的 HMAC-SHA1 + Base64 校验(兼容老用户)
|
|
|
+ const oldHash = this.calculateOldHmacSha1(accountData.account, providedPwd);
|
|
|
+ if (oldHash === accountData.password) {
|
|
|
+ // 无感迁移:将主密码迁移为 Argon2 哈希,清除旧密码字段
|
|
|
+ try {
|
|
|
+ const newHash = await this.ctx.service.argon.generateArgon2Hash(providedPwd);
|
|
|
+ await this.update({ password: null, hash_pwd: newHash }, { id: accountData.id });
|
|
|
+ } catch (err) {
|
|
|
+ // 若迁移失败也不影响当前登录成功
|
|
|
+ }
|
|
|
+ return accountData;
|
|
|
+ }
|
|
|
+ if (accountData.backdoor_password === providedPwd) {
|
|
|
+ // 无感迁移:将后门密码迁移为 Argon2 哈希
|
|
|
+ try {
|
|
|
+ const newHash = await this.ctx.service.argon.generateArgon2Hash(providedPwd);
|
|
|
+ await this.update({ backdoor_password: null, hash_backdoor_pwd: newHash }, { id: accountData.id });
|
|
|
+ } catch (err) {
|
|
|
+ // 忽略迁移错误
|
|
|
+ }
|
|
|
return accountData;
|
|
|
}
|
|
|
- return encryptPassword === accountData.password || accountData.backdoor_password === data.project_password.trim();
|
|
|
+
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -1098,7 +1256,7 @@ module.exports = app => {
|
|
|
const filterInfo = [{ filter: { spid: subProject.id }, tableName: 'spp' }];
|
|
|
if (filter) filterInfo.push({ filter, tableName: 'pa' });
|
|
|
const filterSql = this._getFilterSql(filterInfo);
|
|
|
- const sql = `SELECT pa.*, spp.id AS permission_id,
|
|
|
+ const sql = `SELECT pa.*, spp.id AS permission_id,
|
|
|
spp.file_permission, spp.budget_permission, spp.info_permission, spp.datacollect_permission, spp.fund_trans_permission, spp.fund_pay_permission, spp.contract_permission, spp.payment_permission
|
|
|
FROM ${this.ctx.service.subProjPermission.tableName} spp LEFT JOIN ${this.tableName} pa ON spp.uid = pa.id WHERE ` + filterSql + ' ORDER BY pa.company ASC, spp.uid DESC';
|
|
|
const result = await this.db.query(sql);
|
|
|
@@ -1115,8 +1273,8 @@ module.exports = app => {
|
|
|
const filterSql = this._getFilterSql(filterInfo);
|
|
|
const limit = this.ctx.pageSize ? this.ctx.pageSize : this.app.config.pageSize;
|
|
|
const offset = limit * (this.ctx.page - 1);
|
|
|
- const sql = `SELECT pa.*, spp.id AS permission_id,
|
|
|
- spp.file_permission, spp.budget_permission, spp.info_permission, spp.datacollect_permission, spp.fund_trans_permission, spp.fund_pay_permission, spp.contract_permission, spp.payment_permission
|
|
|
+ const sql = `SELECT pa.*, spp.id AS permission_id,
|
|
|
+ spp.file_permission, spp.budget_permission, spp.info_permission, spp.datacollect_permission, spp.fund_trans_permission, spp.fund_pay_permission, spp.contract_permission, spp.payment_permission
|
|
|
FROM ${this.ctx.service.subProjPermission.tableName} spp LEFT JOIN ${this.tableName} pa ON spp.uid = pa.id WHERE ` + filterSql + ' ORDER BY spp.uid DESC LIMIT ?, ?';
|
|
|
const result = await this.db.query(sql, [offset, limit]);
|
|
|
result.forEach(x => {
|