用Python生成重置令牌的最佳方法是什么?

17

我正在尝试为密码重置制定一个验证流程,使用了两个值:Unix时间戳和用户旧密码(pbkdf2)作为密钥。

由于我不想得到非ASCII字符,所以我使用了SimpleEncode库,因为它只是使用密钥的BASE64编码,速度很快,但问题是密码太长了(196个字符),所以密钥也很长!

我所做的是分割结果code = simpleencode.encode(key,asci)[::30],但这将不是唯一的!

想了解Facebook重置过程的工作原理,但给出的是一个数字!那么这个过程是如何工作的?他们不使用密钥使重置链接难以被篡改吗?

更新:算法的工作原理:

1- 使用Unix时间戳获取时间 time.time()

2- 生成Unix时间戳的Base64编码(用于URL)和Unix时间戳值+一个密钥的Base64编码,此密钥为PBKDF2(password)。

3- 生成URL www.example.com/reset/user/Base64(time.time()) 并发送此URL + simpleencode.encode(key,asci)[::30]

4- 当用户单击URL时,将输入生成的代码,如果此生成的代码与URL匹配,则允许他修改密码,否则,它是一个无效URL!


1
如果您确认它不在数据库中,并且足够长,以至于没有人能够猜测它,那么它就不需要是唯一的。 - Wooble
1
作为重置令牌的一部分,您不应以任何形式公开(哈希值)用户密码。这将允许攻击者学习哈希值,然后尝试暴力破解。 - zwol
1
您正在暴露PBKDF2(密码)。如果“密码”很弱,那么这是可以通过暴力破解来逆转的,如果它实际上是用户的密码,那么它很可能是弱密码。 - zwol
1
@AbdelouahabPp 这仍然是一个安全漏洞,而一个安全漏洞只有在被利用之前才不太可能被利用。尽量避免引入它们。您始终可以使用与存储密码时使用的不同盐值来安全地哈希密码。 - millimoose
1
@AbdelouahabPp,此外,30个字符的base64编码的随机数据(即哈希)绝对足够“独特”。 - millimoose
显示剩余7条评论
3个回答

34

我不确定这是否是最好的方法,但我可能只会生成一个UUID4,在URL中使用它来重置密码并在“n”时间后过期。

>>> import uuid
>>> uuid.uuid4().hex
'8c05904f0051419283d1024fc5ce1a59'

您可以使用类似于http://redis.io的东西来保存该密钥,将其值设置为相应的用户ID并设置其生存时间。因此,当来自http://example.com/password-reset/8c05904f0051419283d1024fc5ce1a59的内容被检测到有效时,就允许更改为新密码。

如果您确实需要“验证PIN码”,那么可以与令牌一起存储一个小的随机密钥,例如:

>>> from string import digits
>>> from random import choice
>>> ''.join(choice(digits) for i in xrange(4))
'2545'

并请求将其输入重置链接中。


1
@AbdelouahabPp,我个人认为你的方法过于复杂化了问题——而且没有理由使用任何关于用户的信息来生成令牌... - Jon Clements
1
如果你的意思是“伪造”,那么生成一个已发行的令牌的概率非常小(1/340,282,366,920,938,463,463,374,607,431,768,211,456),更不用说在它仍然有效的情况下了。不过,我已经添加了一个简单的 PIN 示例,如果这能让你感到更舒适的话... - Jon Clements
我不建议这样做。random 库的输出不安全且非常容易被预测。使用安全的随机数,例如 secrets 模块(3.6 版本)或 os.urandom(<3.6 版本)。 - Azsgy
uuid 是可以的,因为它使用了 os.urandom,但是 PIN 方案可能存在潜在的危险。 - Azsgy
非常抱歉,我之前只是粗略地浏览了一下,看到有人在令牌中使用了“random”,感到有些惊讶,但事实证明这并不是很准确。不过,在有人决定在没有令牌的情况下使用它之前,最好先加上免责声明。 - Azsgy
显示剩余7条评论

23

最简单的方法是使用ItsDangerous库:

你可以将用户ID序列化并签名为取消订阅电子邮件的URL。这样,您就不需要生成一次性令牌并将其存储在数据库中。同样适用于任何类型的帐户激活链接和类似事项。

您还可以嵌入时间戳,因此非常容易设置时间段,而无需涉及数据库或队列。所有内容都经过加密签名,因此您可以轻松查看是否被篡改。

>>> from itsdangerous import TimestampSigner
>>> s = TimestampSigner('secret-key')
>>> string = s.sign('foo')
>>> s.unsign(string, max_age=5)
Traceback (most recent call last):
  ...
itsdangerous.SignatureExpired: Signature age 15 > 5 seconds

有人能告诉我timestampsigner是如何工作的吗?我正在开发一个Web应用程序,需要生成基于时间的忘记密码链接,并且链接在一段时间后应该过期。 - Mandava Geethabhargava
如果您需要查看其实际工作方式,可以查看库的源代码 https://github.com/pallets/itsdangerous/blob/master/src/itsdangerous/timed.py - Doobeh

4

为什么不使用JWT作为此目的的令牌,它也可以设置到期时间,因此也可以将到期日期放入令牌中。

  1. 使用秘密密钥生成令牌(JWT)
  2. 发送包含令牌作为查询参数的链接的电子邮件(当用户打开链接时,页面可以读取令牌)
  3. 在保存新密码之前验证令牌

为了生成JWT令牌,我使用pyjwt。下面的代码片段显示如何使用24小时(1天)的到期时间并使用秘密密钥进行签名:

import jwt
from datetime import datetime, timedelta, timezone

secret = "jwt_secret"
payload = {"exp": datetime.now(timezone.utc) + timedelta(days=1), "id": user_id}
token = jwt.encode(payload, secret, algorithm="HS256")
reset_token = token.decode("utf-8")

以下代码片段展示了如何在Django中设置令牌和新密码的验证。如果令牌已过期或被篡改,它将引发异常。
secret = "jwt_secret"
claims = jwt.decode(token, secret, options={"require_exp": True})
# Check if the user exists
user = User.objects.get(id=claims.get("id"))
user.set_password(password)
user.save()

感谢您提供这个额外的答案。如果用户 ID(在 JWT 中)是由客户端提供的(步骤 2,验证和存储新密码),是否存在任何缺点? - Freude
应该没问题,因为您的后端代码将发送带有重置链接的电子邮件,即使恶意用户输入了其他人的ID,也只有正确的用户才能访问他的电子邮件收件箱,并且只有他能够单击重置链接。 - lordvcs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接