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

开发环境
-
- 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/时…

非常成功!!!尽管此次只有5秒,但通过更改View文件中的EXPIRED_SECONDS,可以生成具有任意有效期限的链接。
请参考Django官方网站以获取详细信息。
非常感谢!