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

17 投票
3 回答
14007 浏览
提问于 2025-04-17 14:51

我正在尝试制作一个密码重置的验证过程,我用到了两个值:一个是时间戳,另一个是用户的旧密码(用pbkdf2加密)作为密钥。

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

我做的事情是把结果分割一下 code = simpleencode.encode(key,asci)[::30],但是这样得到的结果就不唯一了!

为了理解这个过程是怎么工作的,我试过Facebook的重置流程,但他们给出的只是一个数字!那么这个过程是怎么运作的呢?难道他们不使用密钥来防止别人伪造重置密码的链接吗?

更新:算法将如何工作:

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

2- 生成时间戳的Base64编码(用于URL),然后将时间戳值和一个密钥结合,这个密钥就是PBKDF2(密码)。

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

4- 当用户点击这个URL时,他需要输入生成的代码,如果这个代码和URL匹配,那么就允许他修改密码,否则就是一个无效的链接!

3 个回答

4

为什么不直接用JWT作为这个目的的令牌呢?其实可以给链接设置一个过期时间,就像给令牌设置过期日期一样。

  1. 生成一个用秘密钥匙加密的令牌(JWT)
  2. 发送一封邮件,里面包含一个带有令牌的链接(当用户打开这个链接时,页面可以读取到这个令牌)
  3. 在保存新密码之前验证这个令牌

我用pyjwt来生成JWT令牌。下面的代码片段展示了如何生成一个过期时间为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()
23

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

你可以把用户的ID进行处理,然后把它放到取消订阅新闻邮件的链接里。这样你就不需要生成一次性的令牌,也不用把它们存储在数据库里。对于账户激活链接和类似的东西也是一样的。

你还可以加上时间戳,这样就可以很简单地设置时间限制,而不需要用到数据库或队列。所有的内容都是经过加密签名的,所以你可以很容易地检查它是否被篡改过。

>>> 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
34

我不确定这是不是最好的方法,但我可能会生成一个UUID4,这个东西可以用在重置密码的链接里,并且在一段时间后让它失效。

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

你可以使用类似于http://redis.io的工具来保存这个密钥,密钥的值是对应的用户ID,并设置它的有效时间。这样,当有人访问http://example.com/password-reset/8c05904f0051419283d1024fc5ce1a59时,系统会检查这个链接是否有效,如果有效,就允许用户设置一个新密码。

如果你想要一个“验证码”,那么可以在存储这个令牌的同时,保存一个小的随机密钥,比如:

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

然后要求用户在重置链接中输入这个验证码。

撰写回答