只使用Django的标准功能来创建带有截止日期的链接

这次我们尽量只使用Django的标准功能来创建一个带有期限限制的页面。就像在用户注册时经常见到的那种在30分钟内完成的东西。

做好的东西

スクリーンショット 2018-12-03 20.49.47.png

开发环境

    • macOS High Sierra 10.13.6

 

    • Docker for Mac

Engine : 18.09.0

Python : 3.7
Django : 2.1

环境建设

我们从创建Docker镜像开始吧。

.
├── Dockerfile
└── requirements.txt

本次我们正在使用Docker来运行Django。

FROM python:3.7

ENV APP_PATH /opt/apps

COPY requirements.txt $APP_PATH/
RUN pip install --no-cache-dir -r $APP_PATH/requirements.txt

WORKDIR $APP_PATH
Django==2.1

我从上述的Dockerfile中创建了一个名为django2.1的镜像。

$ docker build -t django2.1 ./

使用先前创建的图像创建Django应用程序的框架。

$ docker run --rm \
--mount type=bind,src=$(pwd),dst=/opt/apps \
django2.1 \
django-admin startproject my_docker_project .

指令有点长,但每个选项在这篇文章中都有解释。

我认为,当到达这个地方时,目录看起来应该是这样的。

.
├── Dockerfile
├── manage.py
├── my_docker_project
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── requirements.txt

接下来,我们将添加一个生成有期限的URL的应用程序。

$ docker run --rm \
--mount type=bind,src=$(pwd),dst=/opt/apps \
django2.1 \
python manage.py startapp timestamp_signer

这次我们创建了一个名为”timestamp_signer”的东西。为了让Django能够识别它,我们需要修改./my_docker_project/settings.py和./my_docker_project/urls.py文件。

#~中略~
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'timestamp_signer', #追加!
]
#~中略~
from django.contrib import admin
from django.urls import path,include #追加!

urlpatterns = [
    path('',include('timestamp_signer.urls')) ,#追加!
    path('admin/', admin.site.urls),
]

辛苦了。
以下是接下来关于限时链接的重点部分,可能会有点长。

尝试创建一个有期限的URL。

首先我们要定义路由。

from django.urls import path
from timestamp_signer import views

app_name = 'timestamp_signer'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'), #URL生成用
    path('<str:token>/', views.IndexView.as_view(), name='index'),#URL検証用
]

为了简单起见,这次的处理方式是无论是像localhost:8000这样的请求还是像localhost:8000/hogehoge/这样的请求,都会使用相同的视图进行处理。

接下来是视角。
说实话,只要看这一部分,基本上可以了解我在这篇文章中想要传达的内容。

from django.core.signing import TimestampSigner,BadSignature,SignatureExpired
from django.shortcuts import render
from django.views import View

import random
import string
from datetime import timedelta

EXPIRED_SECONDS = 5

class IndexView(View):
    template_name = 'timestamp_signer/index.html'
    timestamp_signer = TimestampSigner()

    def get_random_chars(self,char_num=30):
         return ''.join([random.choice(string.ascii_letters + string.digits) for i in range(char_num)])

    def get(self,request,token=None):
        context = {}
        context['expired_seconds'] = EXPIRED_SECONDS

        if token:
            try:
                unsigned_token = self.timestamp_signer.unsign(
                    token,
                    max_age=timedelta(seconds=EXPIRED_SECONDS)
                )
                context['message'] = '有効なトークンです!!!'
            except SignatureExpired:
                context['message'] = 'このトークンは期限切れです。'
            except BadSignature:
                context['message'] = 'トークンが正しくありません。'

        return render(request, self.template_name, context)

    def post(self,request):
        context = {}
        context['expired_seconds'] = EXPIRED_SECONDS
        token = self.get_random_chars()
        token_signed = self.timestamp_signer.sign(token)
        context['token_signed'] = token_signed
        return render(request, self.template_name, context)

本篇重要的部分是TimestampSigner模块。让我们立即在控制台上进行操作验证吧。

$ docker run --rm -it \
--mount type=bind,src=$(pwd),dst=/opt/apps \
django2.1:latest \
python manage.py shell
>>> from django.core.signing import TimestampSigner
>>> timestamp_signer = TimestampSigner()
>>> token_signed = timestamp_signer.sign('hoge')
>>> token_signed
'hoge:1gTokx:KLGAVyLSEA0ZF6r9FV3GNQsmqfY'

当使用TimestampSigner实例的sign()方法传递某个字符串时,它似乎会以签名的形式返回。

要验证得到的字符串,可以使用TimestampSigner类的unsign()实例方法。

>>> token_unsigned = timestamp_signer.unsign(token_signed)
>>> token_unsigned
'hoge'
>>> token_signed += 'abc' #署名文字列を改変!
>>> timestamp_signer.unsign(token_signed)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/usr/local/lib/python3.7/site-packages/django/core/signing.py", line 187, in unsign
    result = super().unsign(value)
  File "/usr/local/lib/python3.7/site-packages/django/core/signing.py", line 170, in unsign
    raise BadSignature('Signature "%s" does not match' % sig)
django.core.signing.BadSignature: Signature "1gTokx:KLGAVyLSEA0ZF6r9FV3GNQsmqfYabc" does not match

第一个例子中,我们成功验证并返回了原始字符串’hoge’,而在第二个修改后的字符串中,我们确认发生了BadSignature错误。

接下来,让我们为token_signed设置一个有效期限。

>>> from datetime import timedelta
>>> token_signed = timestamp_signer.sign('hoge')
>>> timestamp_signer.unsign(token_signed,max_age=timedelta(seconds=10))
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/usr/local/lib/python3.7/site-packages/django/core/signing.py", line 197, in unsign
    'Signature age %s > %s seconds' % (age, max_age))
django.core.signing.SignatureExpired: Signature age 22.781551599502563 > 10.0 seconds
>>> timestamp_signer.unsign(token_signed,max_age=timedelta(seconds=1000))
'hoge'

第一个例子中,当将max_age设为10秒时,出现了SignatureExpired错误;
第二个例子中,当将max_age设为1000秒时,我们确认验证成功了。

通过将指定秒数的timedelta对象传递给unsign方法的max_age参数,我们发现可以判断生成的签名字符串是多久以前发行的!

现在我们再来看一下刚才的GET和POST方法。

def get(self,request,token=None):
    context = {}
    context['expired_seconds'] = EXPIRED_SECONDS

    if token:
        try:
            #URLに含まれる文字列を検証
            unsigned_token = self.timestamp_signer.unsign(
                token,
                max_age=timedelta(seconds=EXPIRED_SECONDS)
            )
            context['message'] = '有効なトークンです!!!'
        except SignatureExpired:
            context['message'] = 'このトークンは期限切れです。'
        except BadSignature:
            context['message'] = 'トークンが正しくありません。'

    return render(request, self.template_name, context)

我认为您能够看出它在验证URL中包含的字符串,并根据错误类型和存在与否切换消息。

def post(self,request):
    context = {}
    context['expired_seconds'] = EXPIRED_SECONDS
    token = self.get_random_chars() #ランダムな文字列を生成
    token_signed = self.timestamp_signer.sign(token) #文字列を署名
    context['token_signed'] = token_signed
    return render(request, self.template_name, context)

当收到POST请求之后,我们会生成一个签名字符串,并将其传递给模板端。

使用模板完成!

我们快点走吧。
这次我们使用了Bulma作为CSS框架。

{% load static %}
<!DOCTYPE html>
<html>
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>timestamp_signer_sample</title>
   <link href="{% static 'vendor/bulma/css/bulma.min.css' %}" rel="stylesheet" type="text/css">
 </head>
 <body>
   <section class="section">

    <div class="container">
      <form action="{% url 'timestamp_signer:index' %}" method="post">
        {% csrf_token %}
        <p style="margin-bottom:10px;">{{ expired_seconds }}秒間だけ有効なURLを生成します。</p>
        <button class="button is-primary" type="submit">Create URL</button>
      </form>

      <div style="margin-top:20px;">
        {% if token_signed %}
          <a href="{% url 'timestamp_signer:index' token_signed %}">{{token_signed}}</a>
        {% endif %}

        {% if message %}
          <h1> {{ message }} </h1>
        {% endif %}
      </div>

    </div>
  </section>
 </body>
</html>

结束

辛苦了!我们马上在本地启动它吧!

$ docker run --rm -it \
--mount type=bind,src=$(pwd),dst=/opt/apps \
-p 8000:8000 \
django2.1:latest \
python manage.py runserver 0.0.0.0:8000

当您访问http://0.0.0.0:8000/时…

スクリーンショット 2018-12-05 22.19.26.png

非常成功!!!尽管此次只有5秒,但通过更改View文件中的EXPIRED_SECONDS,可以生成具有任意有效期限的链接。

请参考Django官方网站以获取详细信息。

非常感谢!

广告
将在 10 秒后关闭
bannerAds