今回はなるべくDjangoの標準機能のみを使って,
期限(制限時間)付きのページを作ってみたいと思います。
ユーザ登録時とかによく見る**30分以内に〜**みたいなやつです。
##作ったもの
「Create URL」で期限付きのURLリンクを生成して、
リンク先が有効なURLかを検証してくれるといったものです。
ユーザ登録を必要とするアプリケーションなどではあるあるな要件だと思いますので、
生成したリンクはメール文面に貼り付けるなど、よしなに応用してみてください。
##開発環境
- 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についてです。
##期限付き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/'のようなリクエストでも同じViewでさばいています。
次にViewです。
ぶっちゃけここだけ読んでいただければこの記事で伝えたいことはだいたい終わります。
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リクエストを受け取った時点で署名文字列を生成し、テンプレート側に渡しています。
##テンプレートで仕上げ!
さくっと行きましょう。
今回はCSSフレームワークにBulmaを使いました。
{% 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公式のここを参照いただければと思います。
ありがとうございました!