从 UUID 或 HMAC/JWT/hash 生成一次性安全令牌?
Posted
技术标签:
【中文标题】从 UUID 或 HMAC/JWT/hash 生成一次性安全令牌?【英文标题】:Generating one-time-only security tokens from UUID or HMAC/JWT/hash? 【发布时间】:2018-06-18 15:58:43 【问题描述】:我正在为 Web 应用程序构建后端。当新用户访问该站点并单击注册按钮时,他们会填写一个超级简单的表单,询问他们的用户名和密码,然后他们就会提交。这会提示服务器向该电子邮件地址发送验证电子邮件。然后他们会检查他们的电子邮件,单击一个链接(用于验证他们的电子邮件),然后被路由到登录页面,以便他们可以选择登录。
为了验证他们的电子邮件,当服务器生成电子邮件时,它需要创建(并存储)一个验证令牌(可能是 UUID)并将其附加到电子邮件中的此链接,使链接看起来像:
“https://api.myapp.example.com/v1/users/verify?vt=12345”
vt=12345
是“验证令牌”(也可能是 UUID)。因此,用户单击此链接,我的GET v1/users/verify
端点查看令牌,以某种方式确认其有效,并进行一些数据库更新以“激活”用户。他们现在可以登录了。
当用户想要取消订阅接收电子邮件或忘记密码并需要恢复密码以便登录时的类似场景。
退订
用户想停止接收电子邮件但仍想使用该应用程序。他们在我们发送给他们的每周时事通讯中单击“退订”链接。该链接需要包含某种类似的“取消订阅令牌”,就像上面的验证令牌一样,生成并存储在服务器上,用于验证用户取消订阅电子邮件的请求。
找回密码
这里用户忘记了密码,需要找回密码。因此,在登录屏幕上,他们单击“忘记我的密码”链接,然后会看到一个表格,他们必须在其中填写他们的电子邮件地址。服务器向该地址发送电子邮件。他们检查了这封电子邮件,其中包含一个表格链接,他们可以在其中输入新密码。此链接需要包含一个“重置密码令牌”——就像上面的验证令牌一样——生成并存储在服务器上,用于验证用户更改密码的请求。
所以这里我们要解决三个非常相似的问题,都需要使用我所说的“一次性 (OTO) 安全令牌”。这些 OTO 令牌:
必须在服务器端生成并持久化(可能保存到security_tokens
表中)
必须是可以附加到我们将从电子邮件内部公开的链接的内容
必须只有一次有效:一旦他们点击它,令牌就被“使用”并且不能被重复使用
我的问题
我想出的解决方案很简单……几乎太简单了。
对于令牌,我只是生成随机 UUID(36 字符)并将它们存储到具有以下字段的 security_tokens
表中:
[security_tokens]
---
id (PK)
user_id (FK to [users] table)
token (the token itself)
status (UNCLAIMED or CLAIMED)
generated_on (DATETIME when created)
当服务器创建它们时,它们是“UNCLAIMED”。当用户单击表内的链接时,他们被“认领”。后台工作人员作业将定期运行以清理任何 CLAIMED 令牌或删除任何已“过期”的 UNCLAIMED 令牌(基于其generated_on
字段)。该应用程序还将忽略之前已声明(并且尚未清理)的任何令牌。
我认为这个解决方案可行,但我不是一个超级安全的人,我担心这种方法:
-
可能会使我的应用程序受到某种类型的攻击/利用;和
当其他解决方案可能同样有效时,可能会重新发明***
就像上面的第二个一样,我想知道我是否应该使用与哈希/HMAC/JWT 相关的机制而不是死的简单 UUID。也许有一些聪明的加密/安全人员找到了一种方法,使这些令牌本身以安全/不可变的方式包含 CLAIM 状态和到期日期等。
【问题讨论】:
【参考方案1】:你在正确的路线上
根据我希望它执行的操作,我在我的应用程序中有一个非常相似的方法。我有一个包含每个用户的表(一个用户表),我可以使用它来引用每个单独的帐户并根据他们的身份执行操作。通过添加用户帐户和自我管理选项可以缓解很多的安全威胁。以下是我解决其中一些漏洞的方法。
验证您的电子邮件
当用户注册时,服务器应该使用RNGCryptoServiceProvider()
类来生成一个长度足够长的随机盐,它永远不会被实际猜到。然后,我对盐进行散列(单独)并对它应用 base64 编码,以便可以将其添加到 Url。通过电子邮件将完整的链接发送给用户,并确保将该哈希与相关的UserId
存储在Users
表中。
用户在收件箱中看到一个漂亮而简洁的“单击此处验证您的电子邮件地址”,然后可以单击该链接。它应该重定向到一个接受可选 url 参数的页面(例如mywebsite.com/account/verifyemail/myhash
,然后检查服务器端的哈希值。然后站点可以根据它存储在数据库中的激活哈希值检查哈希值。如果它匹配记录,那么您应该将Users.EmailVerified
列标记为true
并提交到表中。然后,您可以从表中删除该验证记录条目。
干得好,您已成功验证用户的电子邮件地址是真实的!
重设密码
在这里,我们实现了一个类似的方法。但是,我们最好将记录存储在 PasswordResetRequest 表中,而不是验证记录,并且不要删除记录 - 这可以让您查看密码是否被重置以及何时重置。每次用户请求重置密码时,您应该显示一条匿名消息,例如“一封电子邮件已发送到您的主电子邮件地址,其中包含进一步的说明”。即使没有发送或帐户不存在,它也会阻止潜在的攻击者枚举用户名或电子邮件地址以查看它们是否已在您的服务中注册。同样,如果它们是真实的,请使用与以前相同的方法发送链接。
用户打开他们的电子邮件地址并点击链接。然后将它们重定向到重置页面,例如mywebsite.com/account/resetpassword/myhash
。然后,服务器针对数据库运行 url 中的哈希,如果结果是真实的,则返回结果。现在,这是棘手的部分 - 你不应该长时间保持这些活动。我建议使用一列将哈希链接到Users.UserId
,一个名为ExpiraryDateTime
的列包含Datetime.Now.AddMinutes(15)
之类的内容(这使得以后更容易使用),还有一个名为IsUsed
作为布尔值(false by默认)。
点击链接时,您应该检查链接是否存在。如果没有,请给他们默认的“那个链接有问题。请请求一个新的”文本。但是,如果链接有效,您应该检查Used == false
,因为您不希望人们多次使用同一个链接。如果不使用,那就太好了!让我们检查一下它是否仍然有效。最简单的方法是简单的if (PasswordResetRequest.ExpiraryDateTime < DateTime.Now)
- 如果链接仍然有效,那么您可以继续进行密码重置。如果不是,则表示它是在不久前生成的,您不应该再使用它。说真的,有些网站今天仍然允许您生成链接,如果您的电子邮件在 1 个月后被黑客入侵,您仍然可以使用重置链接!
我还应该提到,每次用户请求重置密码时,您应该检查表中的现有记录以获取有效链接。如果一个有效(意味着它仍然可以使用),那么您应该立即将其无效。将哈希替换为一些辅助文本,例如“无效:用户请求新的重置链接”。这也让您知道他们已经请求了多个链接,同时也使他们的链接无效。如果您真的想防止人们尝试使用过期的链接,您也可以将其标记为已使用,因为您很聪明并将整个“无效:用户请求的新重置链接”作为编码 URL 偷偷进入他们的浏览器。同一个帐户不应该有多个处于活动状态的重置链接 - 永远!
退订
为此,我在数据库中有一个简单的标志来确定用户是否可以接收促销优惠和新闻通讯等。所以Users.SubscribedToNewsletter
就足够了。他们应该能够登录并在他们的电子邮件设置或通信首选项等中进行更改。
一些代码示例
这是我在 C# 中的 RNGCryptoServiceProvider 代码
public static string GenerateRandomString(RNGCryptoServiceProvider rng, int size)
var bytes = new Byte[size];
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
var rng = new RNGCryptoServiceProvider();
var randomString = GenerateRandomSalt(rng, 47); // This will end up being a string of almost entirely random bytes
为什么要使用 RNGCryptoServiceProvider?
RNGCryptoServiceProvider()
(它是其安全库中的 C# 类)允许您根据完全随机且不可重现的事件生成看似随机的字节串。像Random()
这样的类仍然需要使用某种内部数据来根据可预测的算法事件(例如当前日期和时间)生成一个数字。 RNGCryptoServiceProvider()
使用诸如 cpu 温度、正在运行的进程数等所有内容来创建无法复制的随机内容。这允许最终的字节数组尽可能随机。
我为什么要 Base64 编码?
Base64 编码将产生一个只包含数字和字母的字符串。这意味着文本中没有符号或编码字符,因此在 URL 中使用是安全的。这不是一个安全功能,但它确实允许您在方法的参数中只允许数字和字母,并过滤掉或拒绝任何不符合此标准的输入。例如,过滤掉任何包含人字形 <
和 >
的输入应该可以防止 XSS。
注意事项
您应该始终假设包含您的哈希的链接是无效,直到您对其执行每次检查以确保它通过要求。因此,您可以执行各种if
语句,但除非您传递每一个,否则您会将默认的下一步操作留给用户某种形式的错误。为了澄清,我应该检查密码重置链接是否有效,然后未使用,然后仍在时间窗口内,然后执行我的重置操作。如果它未能通过任何这些要求,默认操作应该是给用户一个错误,说它是一个无效的链接。
给他人的备注
由于我非常有信心这不是唯一实现此目的的方法,我只想声明这是我多年来一直这样做的方式,从未让我失望过并让我的公司通过了几次广泛的渗透测试。但是,如果有人有更好/更安全的方法,请做一些说明,因为我很乐意了解更多信息。如果您对我提到的特定部分有任何其他问题或需要澄清,请告诉我,我会尽力提供帮助
【讨论】:
感谢@Horkrine (+1) - 惊人的 + 周到的回答!!!如果您不介意,我会按照您在此处所说的所有内容为您提供两个超级快速的后续问题! (1) 您生成 base64 编码的RNGCryptoServiceProvider
-salted 哈希与我的随机 UUID 的方法,那里真正的安全差异是什么?仅仅是盐渍哈希比 36 字符 UUID 更难猜测/破解吗?并且 (2) 您是否有机会通过快速 sn-p 更新您的答案,以了解您如何生成 RNGCryptoServiceProvider
-salted 哈希值以及它使用什么语言?再次非常感谢!!!
所以,我刚刚意识到这不在 C# 下。我仍然会编辑答案,但我输入的代码将在 C# 中 - 你仍然应该能够在你使用的任何东西中做类似的事情。我会为你添加我的 C# sn-ps 以及一些链接。
已编辑。如果您还有其他需要,请告诉我
太棒了!再次感谢@Horkrine (+1)。使用 JWT 来做到这一点怎么样?我可能可以将用户 ID 和到期日期填充到 JWT 的签名数据中,唯一的缺点是令牌可以在到期之前重复使用,我认为这对于这 3 种情况来说都可以,不是吗?
@Horkrine 为什么最后一行读 GenerateRandomSalt 不应该是 GenerateRandomString?以上是关于从 UUID 或 HMAC/JWT/hash 生成一次性安全令牌?的主要内容,如果未能解决你的问题,请参考以下文章