0

我正在开发一个 api,其中包括发送电子邮件以进行密码重置和电子邮件确认。以及“用户注册”等功能。

我正在尝试包含一个操作限制器,允许用户在给定的时间范围内执行这些操作的次数有限,以防止恶意使用。

起初我认为使用 IP 地址会很好,因为即使是恶意用户最终也会用完 IP 地址(至少我知道),但后来我意识到这可能会阻止在大型建筑物中的用户,并可能给 VPN 用户带来不便。

唯一标识未登录用户以限制他们对某些功能的操作的最佳方法是什么?这可能吗?FAANG 是如何处理这个问题的?

这是我在 nodejs 中编写的一个示例,如果有人对如何使其更加独特有任何反馈和/或想法,我会全力以赴。

const db = require('../../common/database');
// const ActionLimiterEnum = require('../../enums/action-limiter').actionLimiterEnum;
const NumberUtil = require('../../utils/number');
const ObjectUtil = require('../../utils/object');


// !==========================================================================================!
// This module has been put on hold until I can think of a way to uniquely identify users
// Major problem about this is that it may deny large groups of people whom use the same ip 
// VPNS and/or large buildings
//
// Could cause more problems than it solves
// !==========================================================================================!



// Simple action limiter for how often a user can perform actions
// Needs to be saved to a database and not an instance because there may be multiple instances and/or they may be reset

// Object of action or "signIn"
async function actionLimiter(action,ip){
  const d = {err: {code:0,message:""},res:{}}; let r,sql,vars;

  r = await checkLimit(action,ip);
  if(r.err.code) return r;

  r = await incrementLimit(action,ip);
  if(r.err.code) return r;

  return d;
}

async function checkLimit(action,ip){
  const d = {err: {code:0,message:""},res:{}}; let r,sql,vars;

  if(action === "signIn"){
    r = await checkLimit(ActionLimiterEnum.signInShortTerm,ip); if(r.err.code) return r;
    r = await checkLimit(ActionLimiterEnum.signInMidTerm,ip); if(r.err.code) return r;
    r = await checkLimit(ActionLimiterEnum.signInLongTerm,ip); if(r.err.code) return r;
    return d;
  }

  const numberIp = NumberUtil.ipToNumber(ip);

  var deleteDate = new Date();
  deleteDate.setMilliseconds(deleteDate.getMilliseconds() - action.time);

  sql = "DELETE FROM m_admin_action_limiter WHERE action_id = ? AND created_date <= ?";
  vars = [action.id,deleteDate];
  r = await db.query(sql,vars);
  if(r.err.code) return r;

  sql = "SELECT * FROM m_admin_action_limiter WHERE action_id = ? AND ip = ?";
  vars = [action.id,numberIp];
  r = await db.query(sql,vars);
  if(r.err.code) return r;

  if(r.res.length){
    const results = ObjectUtil.toCamelCaseKeys(r.res[0]);
    if(results.actionCount >= action.maxCount){
      d.err.code = 1;
      d.err.message = "Sorry this ip has performed this action too often please try again later. ";

      switch(action.id){
        case ActionLimiterEnum.signInShortTerm.id:
        case ActionLimiterEnum.signInMidTerm.id:
        case ActionLimiterEnum.signInLongTerm.id:
          d.err.message += "If you're having trouble remembering your password you can reset it via email. ";
          break;
        default: 
          break;
      }

      d.err.actionLimited = true;
      return d;
    }
  }

  return d;
}

async function incrementLimit(action,ip){
  const d = {err: {code:0,message:""},res:{}}; let r,sql,vars;

  if(action === "signIn"){
    r = await incrementLimit(ActionLimiterEnum.signInShortTerm,ip); if(r.err.code) return r;
    r = await incrementLimit(ActionLimiterEnum.signInMidTerm,ip); if(r.err.code) return r;
    r = await incrementLimit(ActionLimiterEnum.signInLongTerm,ip); if(r.err.code) return r;
    return d;
  }

  const numberIp = NumberUtil.ipToNumber(ip);
  const timenow = new Date();

  sql = "SELECT admin_action_limiter_id FROM m_admin_action_limiter WHERE action_id = ? AND ip = ?";
  vars = [action.id,numberIp];
  r = await db.query(sql,vars);
  if(r.err.code) return r;

  if(r.res.length){
    // update
    const id = r.res[0]['admin_action_limiter_id']

    sql = "UPDATE m_admin_action_limiter SET action_count = action_count + 1 WHERE admin_action_limiter_id = ?";
    vars = [id];
    r = await db.query(sql,vars);
    if(r.err.code) return r;
  }else{
    // insert
    sql = "INSERT INTO m_admin_action_limiter (action_id,ip,action_count,created_date) VALUES(?,?,?,?)";
    vars = [action.id,numberIp,1,timenow];
    r = await db.query(sql,vars);
    if(r.err.code) return r;
  }

  return d;
}

module.exports = {
  actionLimiter, 
  checkLimit,
  incrementLimit,
  Enum: ActionLimiterEnum,
};



//  SQL

// -- -----------------------------------------------------
// -- Table `m_admin_action_limiter`
// -- -----------------------------------------------------
// CREATE TABLE IF NOT EXISTS `m_admin_action_limiter`(
// `admin_action_limiter_id` int(11) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
// `action_id` int(2) unsigned NOT NULL,
// `ip` int(11) unsigned NOT NULL,
// `action_count` unsigned int(11) DEFAULT 1,
// `created_date` DATETIME NOT NULL
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

// ALTER TABLE `m_admin_action_limiter`
//   ADD CONSTRAINT m_admin_action_limiter_unique UNIQUE (`action_id`,`ip`);
// CREATE INDEX `created_date_index` ON `m_admin_action_limiter` (`created_date`);




// Enums


// time: (days * hours * minutes * seconds * milliseconds)
// time - amount of times they can try within the alotted count
// const actionLimiterEnum = {
//   signInShortTerm: {
//     id: 1,
//     time: (1 * 1 * 60 * 60 * 1000), // 1 hour
//     maxCount: 24,
//   },
//   signInMidTerm: {
//     id: 2,
//     time: (7 * 24 * 60 * 60 * 1000), // 7 days
//     maxCount: 150,
//   },
//   signInLongTerm: {
//     id: 3,
//     time: (120 * 24 * 60 * 60 * 1000), // 120 days
//     maxCount: 840,
//   },
//   authToken: {
//     id: 4,
//     time: (1 * 24 * 60 * 60 * 1000), // 1 day
//     maxCount: 16,
//   },
//   createAccount: {
//     id: 5,
//     time: (90 * 24 * 60 * 60 * 1000), // 90 days
//     maxCount: 3,
//   },
//   passwordCheck: {
//     id: 6,
//     time: (7 * 24 * 60 * 60 * 1000), // 1 week
//     maxCount: 150,
//   },
// }

// module.exports = {
//   actionLimiterEnum,
// };
4

1 回答 1

1

通知用户如果不启用 cookie,该站点将无法正常工作。如果时间戳 cookie 不存在,则在他们进入注册或登录页面时创建它。如果用户请求登录或注册,而您的 cookie 在尝试读取其时间戳时不存在,我们知道他们禁用了 cookie 或者它可能是恶意用户。如果它显然存在,您将比较时间戳并在请求逻辑运行后更新他们的 cookie 时间戳。现在,如果它不存在,请告诉他们启用 cookie,否则网站将无法运行。这将防止恶意使用并将 IP 地址排除在外。如果恶意用户确实是您的问题/担心,解决 IP 地址问题的唯一方法是使用 cookie,或者使用比您目前尝试识别恶意意图更复杂的逻辑(如果它对误报采取行动,可能会在脚下开枪,尽管我不推荐这条路线)。确保您也采取措施保护您的 cookie。

如果您不想走 cookie 路线,您可以在客户端数据上运行逻辑,以尝试通过时区、安装的字体、屏幕分辨率等来识别用户。

无论如何,如今许多网站都需要使用 cookie 来获得完整的网站功能。可能也是这个原因。

您可以在服务器上设置一个简单的键值对数据库。当用户请求时,获取时区、安装的字体、屏幕分辨率等,并将所有数据更改为没有空格的字符串,然后将其转换为强散列(如果字符串中只有一个字符不同,散列就会改变)。生成的哈希将是用于识别用户的密钥。与所述键关联的值将是它们的唯一时间戳,代表它们最后一次访问服务器的时间。此外,请确保您拉的是实际屏幕的宽度和高度,而不是浏览器视口。否则,如果他们甚至能够弄清楚服务器如何识别您,他们可以调整屏幕大小以使自己看起来像一个独特的用户。显然,如果哈希在客户端请求时有所不同,那么假设它是相对安全的'

使用这种方法,您甚至不需要使用 cookie。此外,还有大量的 JS 库可以提供更全面的客户端数据,我建议您查看这些库以构建更强大的唯一哈希值,从而更好地识别客户端。为了更好地将键值对数据库放在代理服务器上,如果满足代理服务器上处理的条件,则允许向服务器发出请求。此外,您可以使用 Cloudflare 之类的服务将其放置在您的代理前面,以防有人尝试对代理服务器进行 DDoS 攻击。如果发生这种情况,您可以获取代理的新 IP 并将其更改为 DNS 上的新 IP。

于 2020-05-22T12:39:36.350 回答