TL;DR:不要为速率限制而烦恼。只需使用您首选的加密 API/库为每个 URL 生成一个安全的随机 128 位(或 192 位)令牌,然后使用base64url 对其进行编码。在 URL 中包含编码的令牌,并将其与关联的用户、表单和过期数据一起存储在安全数据库中。
像 Conor Mancone 一样,我也建议在 URL 中包含一个具有足够熵的单个随机令牌。您显然应该使用加密安全的随机数源来生成这些令牌。
生成 URL 时,您应该将每个令牌连同验证用户身份和显示正确表单所需的任何相关信息一起存储在数据库中。您可能还希望存储创建和/或过期时间戳,以限制 URL 的有效期(从而降低旧电子邮件被泄露的风险),还只是为了允许您从数据库中清除旧记录.
至于什么算作“足够的熵”,精确的下限显然取决于您的用例和威胁模型。特别是,假设您希望在任何给定时间在您的数据库中最多有 2 p个有效 URL,那么您的对手最多可以对您的服务进行 2 q查询,并且他们最多应该有 1-in-2 r成功猜测有效 URL 的机会,您的令牌应该至少p + q + r 位长。
实际上,相当安全的“行业标准”令牌长度为 128 位。假设您一次最多拥有 2 32 个有效 URL,则 128 位令牌将要求攻击者对您的服务进行至少 2 64次查询,以便有 1/2 32的机会猜到一个有效的 URL . 对于大多数目的,即使没有任何速率限制,这也应该绰绰有余。
(切线,128 位令牌长度还允许您在平均遭受第一次令牌冲突之前生成最多约 2 64 个随机令牌。但这有点无关紧要,因为无论如何数据库都允许您检测冲突并处理他们只是通过生成一个新的令牌。)
如果你真的想确定,你可以达到 192 甚至 256 位。例如,一个 192 位的令牌将允许您拥有多达 2 64 个URL,同时需要至少 2 64 个查询才能获得 1/2 64的攻击成功概率。并且 256 位令牌会在此基础上将攻击的难度增加 2 64倍——我并不认为这对于任何现实威胁都是必要的。
至于生成和编码令牌,我建议使用您选择的任何加密 RNG 简单地生成一个随机的 128 位(或 192 位或 256 位)位串,并使用URL-safe Base64对其进行编码。(大多数编程语言运行时应该内置合适的 RNG,或者至少作为库易于安装。如果没有,您的操作系统很可能会提供一个,例如/dev/urandom
在 Unixish 系统上。)这将为一个 128 位标记,一个 32 个字符的字符串用于 192 位标记或一个 43 个字符的字符串用于 256 位标记。正如Conor Mancone的回答所暗示的那样,它比一次生成一个字符的令牌要简单得多。
顺便说一句,如果您碰巧无法访问方便的数据库和/或安全的 RNG,另一种选择是在 URL 本身中包含所有必要的信息(至少是用户 ID、表单 ID 和时间戳)以及这些值的128 位加密消息验证码(使用存储在服务器上的密钥计算和验证)。事实上,这基本上就是JWT对令牌进行身份验证所做的事情,只是需要更多的开销。
请注意,在这种特殊情况下,每个令牌仅对单个用户/表单/时间戳组合有效,攻击者在尝试猜测令牌之前必须选择它,因此有效地p = 0(因为 2 0 = 1)。因此,与使用前面描述的随机令牌方法相比,稍短一些的令牌可以提供相同的有效安全级别。当然,这种长度节省通常通过需要包含在 URL 中的额外参数来平衡。