限制用户对某些功能的操作。唯一标识未登录用户

Limit user actions on certain functions. Uniquely identify unsigned in users

我正在研究 api,其中包括发送用于密码重置和电子邮件确认的电子邮件。以及 "user sign up" 等函数。

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

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

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

这是我在 nodejs 中写的一个例子,如果有人有任何反馈 and/or 关于如何使它更独特的想法,我会洗耳恭听。

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,
// };

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

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

现在很多网站都需要使用 cookie 来实现完整的网站功能;可能也是这个原因。

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

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