如何创建一次性链接

2017-08-25

如何生成一次性链接

Django使用非常有趣的方法来生成密码重置令牌。我不是一个真正的安全专家,我也不熟悉加密算法,但它非常安全可靠。

在我详细阐述一次性链接一代之前,我想讨论Django的 PasswordResetTokenGenerator实现。因为我们正在做的是实际扩展这个特定的类以满足我们的需要。

一般来说,Django会生成一个令牌,而不会将其保留在数据库中。然而,它仍然具有确定给定令牌是否有效的能力。此外,令牌仅对定义的天数有效。

密码重置令牌的默认值为7天,并且可以通过更改其值在settings.py中进行更改PASSWORD_RESET_TIMEOUT_DAYS

该类有两种公共方法:

  • make_token(用户)
  • check_token(用户,令牌)

make_token方法将生成与该用户相关的数据的散列值将改变的密码复位之后。意思是,在用户点击哈希链接并继续进行密码重置后,链接(或哈希)将不再有效:

def _make_hash_value(self, user, timestamp):
    # Ensure results are consistent across DB backends
    login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
    return (
        six.text_type(user.pk) + user.password +
        six.text_type(login_timestamp) + six.text_type(timestamp)
    )

然后,该哈希值用于创建将邮寄给用户的哈希值:

def _make_token_with_timestamp(self, user, timestamp):
        # timestamp is number of days since 2001-1-1.  Converted to
        # base 36, this gives us a 3 digit string until about 2121
        ts_b36 = int_to_base36(timestamp)

        hash = salted_hmac(
            self.key_salt,
            self._make_hash_value(user, timestamp),
        ).hexdigest()[::2]
        return "%s-%s" % (ts_b36, hash)

因此,这两段代码表明:一次性链接,使用 user.passworduser.last_login 生成 token。一旦被用户点击,链接将不再有效。也是功能中SECRET_KEY使用的salted_hmac。所以除非你SECRET_KEY泄漏了,否则不可能重现哈希值。


创建自己的令牌

所以,基本上你将需要一个在使用链接后会改变的信息。最简单的方法是:

tokens.py

from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils import six

class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):
        return (
            six.text_type(user.pk) + six.text_type(timestamp) +
            six.text_type(user.profile.email_confirmed)
        )

account_activation_token = AccountActivationTokenGenerator()

我假装通过一对一的关系拥有一个配置文件模型的用户模型。然后在这个配置文件模型中,我们有一个名为boolean的布尔标志email_confirmed。

为了使用它,我们可以使用与密码重置相同的方法:

urls.py

url(r'^activate_account/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
                views.ActivateAccountView.as_view(), name='activate_account'),

views.py

from django.contrib.auth import login
from django.utils.encoding import force_text
from django.utils.http import urlsafe_base64_decode

from .tokens import account_activation_token

class ActivateAccountView(View):
    def get(self, request, uidb64, token):
        try:
            uid = force_text(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        if user is not None and account_activation_token.check_token(user, token):
            user.profile.email_confirmed = True
            user.save()
            login(request, user)
            return redirect('profile')
        else:
            # invalid link
            return render(request, 'registration/invalid.html')

当然一些时候,可以使用更容易的方法生成一个随机令牌并将其保存在数据库中,并在“使用”之后检查它并使其无效。但是,如果不是这样,您可以通过 Django如何实现密码重置令牌,启发来实现自己的 一次性链接。

https://simpleisbetterthancomplex.com/tutorial/2016/08/24/how-to-create-one-time-link.html