pythonでjwtを使う際に作ったutilについて
作成したjwtのutilを紹介します。
作成したutilですが、まだまだ修正途中のものです。使用する場合は注意してください。
jwtについての詳細についてはこちらを参照してください。
コードと解説の距離が離れると読みづらいと思いますので、できる限りコメントを書きました。
そちらを見てください。
import datetime from datetime import datetime, timedelta import jwt import pytz class JwtContent(object): JWT_EXPIRE_DAY = 1 JWT_ALGORITHM = "HS256" JWT_SECRET_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" JWT_COOKIE_NAME = "Authentication" COOKIE_EXPIRE_FORMAT = "%a, %d-%b-%Y %H:%M:%S GMT" class JwtUtil(object): @staticmethod def create_token(payload, is_revoke=False): """ payloadを使用してjwtトークンを発行する :param is_revoke: Trueにするとtokenを有効期限切れにする :param payload: dict object :return: jwtでエンコードされたトークン """ timezone = pytz.timezone('Asia/Tokyo') now = datetime.now(tz=timezone) if is_revoke: delta = timedelta(days=-JwtContent.JWT_EXPIRE_DAY) else: delta = timedelta(days=+JwtContent.JWT_EXPIRE_DAY) result = now + delta payload["exp"] = result encode_data = jwt.encode( payload, JwtContent.JWT_SECRET_KEY, algorithm=JwtContent.JWT_ALGORITHM ) return encode_data.decode('utf-8') @staticmethod def is_expired_token(jwt_token): if jwt_token is None: return True, None, None try: decode_data = jwt.decode( jwt_token, JwtContent.JWT_SECRET_KEY, algorithms=[JwtContent.JWT_ALGORITHM] ) return False, decode_data, None except jwt.ExpiredSignatureError as e: # 有効期限切れ return True, None, e except jwt.InvalidTokenError as e: # decodeが実行できなかった return True, None, e except Exception as e: return True, None, e @staticmethod def is_expired_session_token(request, cookie_name): """ requestからトークンのクッキーを取り出してそのクッキーの有効期限を判定する :param cookie_name: :param request: :return: 成功時デコードされたデータ(dict object). 失敗時True """ jwt_token = request.COOKIES.get(cookie_name) return JwtUtil.is_expired_token(jwt_token) @staticmethod def set_jwt_cookie(response, jwt_cookie_data, cookie_name): """ responseオブジェクトにjwtトークンキーをセットする jwtのexpireはcommon.config.JWT_EXPIRE_DAYによって変わる また、タイムフォーマットはAsia/Tokyoのゾーンとしている セキュリティのため、httponlyをtrueにしている :param response: :param jwt_cookie_data: :return: """ timezone = pytz.timezone('Asia/Tokyo') expire_datetime = datetime.now(tz=timezone) + timedelta(days=+JwtContent.JWT_EXPIRE_DAY) expire_datetime_str = expire_datetime.strftime( JwtContent.COOKIE_EXPIRE_FORMAT ) response.set_cookie( cookie_name, jwt_cookie_data, expires=expire_datetime_str, httponly=True ) return response @staticmethod def delete_jwt_cookie(response, cookie_name): response.delete_cookie(cookie_name) return response
データが存在しない場合にデータを追加するSQL
テーブル内にデータが存在しない場合に限って、データを追加する方法を紹介します。
クエリについては日本語で解説をするよにも、まずはコードを見たほうが早いと思うので、先に載せます。
こちらが、データを追加するクエリです。
INSERT INTO tag ( name, created_at, modified_at ) SELECT 'test', now(), now() WHERE NOT EXISTS ( SELECT 1 FROM tag WHERE name = 'test' )
上記のクエリは、tagを追加する際に、すでにタグが存在していたら追加しないというクエリです。
これは、select insert文というものです。そして、not existを使って、存在しない場合を条件で追加しています。
where 1で真になるので、データがある場合でinsertが実行されます。
おまけ
一般的なinsert文はこのようなクエリだと思います。
insert into tag ( xxxx, yyyy, zzzz ) values ( 1, 2, 3 )
じゃあ、このクエリにwhereをつければよいのでは?と考えるかもしれませんが、それは構文エラーになります。
一応クエリを載せておきます。
INSERT INTO tag ( name, created_at, modified_at ) values ( 'test', now(), now() ) WHERE NOT EXISTS ( SELECT 1 FROM tag WHERE name = 'test' )
pythonを使ってcsvのファイルを生成する方法
pythonを使ったcsvのファイルを生成する方法を紹介します。
この記事では、csvの読み込みは扱いません。csvファイルを作成する方法を紹介しています。
csvを扱うための便利なライブラリをインストールします。
pip install pandas
csvを読み書きするためにpandasをインストールします。
pandasをインストールするとnumpyもインストールされます。
csvファイルを生成する関数を作成します。
output_csv_root = 'csv/files' def gen_csv(dataset, file_name): df = pd.DataFrame(dataset) file_path = os.path.join(output_csv_root, file_name + '.csv') df.to_csv(file_path)
gen_csv
を実行すると、{output_csv_root}/{filename}.csv
が作成されます。
datasetに指定する値は、二次元の配列を指定します。
[[1,2,3], [4,5,6]]
これがこうなります。
#, 1, 2, 3 1, 1, 2, 3 2, 4, 5, 6
一行目の#, 1, 2, 3
は見出しです。
この見出しは、自分で指定することもできます。
指定する場合は、dataFrameを作成する際にcolumnsを指定します。
df = pd.DataFrame([rows], columns=['A', 'B', 'C'])
また、行番号が必要ない場合は、このようにindex
をFalseにします。
df.to_csv(file_path, index=False)
mecabを使って解析する方法
mecabのインストール方法等はいろいろなサイトに書かれているので、pythonのプログラミング部分を紹介します。
意外と実装について書かれてる記事が少なくてびっくりしました。なので、help()を使って使い方を試しました。
以下の環境で作業しています。
mac osX brewでmecabとmecab-ipadicをインストール mecab-python3 mecab-ipadic-neologd
mecabインスタンスにはparseToNode(self, *args)
やparseToString(self, *args)
があります。
このうち、parseToString(self, *args)
は文字列にパースされるので少し使いづらいです。
なので、parseToNode(self, *args)
を使用します。
mecab = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd') novel_data = novel_list.get(0) node = mecab.parseToNode('Google Analytics(グーグルアナリティクス)は、Googleが無料で提供するWebページのアクセス解析サービス。') while node: surface = node.surface feature = node.feature print(surface, feature) node = node.next
このようなデータを取得できます。
BOS/EOS,*,*,*,*,*,*,*,* Google Analytics 名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス ( 記号,括弧開,*,*,*,*,(,(,( グーグルアナリティクス 名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス ) 記号,括弧閉,*,*,*,*,),),) は 助詞,係助詞,*,*,*,*,は,ハ,ワ 、 記号,読点,*,*,*,*,、,、,、 Google 名詞,固有名詞,一般,*,*,*,Google,グーグル,グーグル が 助詞,格助詞,一般,*,*,*,が,ガ,ガ 無料 名詞,一般,*,*,*,*,無料,ムリョウ,ムリョー で 助詞,格助詞,一般,*,*,*,で,デ,デ 提供 名詞,サ変接続,*,*,*,*,提供,テイキョウ,テイキョー する 動詞,自立,*,*,サ変・スル,基本形,する,スル,スル Webページ 名詞,固有名詞,一般,*,*,*,Webページ,ウェブページ,ウェブページ の 助詞,連体化,*,*,*,*,の,ノ,ノ アクセス解析 名詞,固有名詞,一般,*,*,*,アクセス解析,アクセスカイセキ,アクセスカイセキ サービス 名詞,サ変接続,*,*,*,*,サービス,サービス,サービス 。 記号,句点,*,*,*,*,。,。,。 BOS/EOS,*,*,*,*,*,*,*,*
surfaceは単語を取得でき、featureではその解析結果が取得できます。
結果を見るとわかるかと思いますが、BOS/EOS,*,*,*,*,*,*,*,*
というのが出てきています。
これを除きつつ、データを使いやすいように取得します。
def main(): mecab = MeCab.Tagger('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd') node = mecab.parseToNode('Google Analytics(グーグルアナリティクス)は、Googleが無料で提供するWebページのアクセス解析サービス。') node_list = [] while node: surface = node.surface feature = node.feature c = feature.split(',')[0] if not c == 'BOS/EOS': node_list.append({ 'surface': surface, 'feature': feature, }) node = node.next for n in node_list: print(n) if __name__ == '__main__': main()
実行結果がこちらです。
{'surface': '', 'feature': '名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス'} {'surface': '\t', 'feature': '記号,括弧開,*,*,*,*,(,(,('} {'surface': 'グーグルアナリティクス', 'feature': '名詞,固有名詞,一般,*,*,*,Google Analytics,グーグルアナリティクス,グーグルアナリティクス'} {'surface': ')', 'feature': '記号,括弧閉,*,*,*,*,),),)'} {'surface': 'は', 'feature': '助詞,係助詞,*,*,*,*,は,ハ,ワ'} {'surface': '、', 'feature': '記号,読点,*,*,*,*,、,、,、'} {'surface': 'Google', 'feature': '名詞,固有名詞,一般,*,*,*,Google,グーグル,グーグル'} {'surface': 'が', 'feature': '助詞,格助詞,一般,*,*,*,が,ガ,ガ'} {'surface': '無料', 'feature': '名詞,一般,*,*,*,*,無料,ムリョウ,ムリョー'} {'surface': 'で', 'feature': '助詞,格助詞,一般,*,*,*,で,デ,デ'} {'surface': '提供', 'feature': '名詞,サ変接続,*,*,*,*,提供,テイキョウ,テイキョー'} {'surface': 'する', 'feature': '動詞,自立,*,*,サ変・スル,基本形,する,スル,スル'} {'surface': 'Webページ', 'feature': '名詞,固有名詞,一般,*,*,*,Webページ,ウェブページ,ウェブページ'} {'surface': 'の', 'feature': '助詞,連体化,*,*,*,*,の,ノ,ノ'} {'surface': 'アクセス解析', 'feature': '名詞,固有名詞,一般,*,*,*,アクセス解析,アクセスカイセキ,アクセスカイセキ'} {'surface': 'サービス', 'feature': '名詞,サ変接続,*,*,*,*,サービス,サービス,サービス'} {'surface': '。', 'feature': '記号,句点,*,*,*,*,。,。,。'}
いい感じです。
使うときはforでループして、dictからデータを取得すると良いでしょう。
もしくはこのようにデータを追加して、tupleで使用してもよいと思います。
node_list.append( (surface, feature,) )
使用するときはこんな感じです。
for s, f in node_list: print(s, f)
DjangoでCSRFを含んだリクエストをtagを使って実行する方法
CSRFトークンを含んだAPIの実行方法で、ドキュメントに載っているような方法ではなく、tagを使った方法を紹介します。
tagを使った方法というのは、{% cookie 'csrftoken' %}
でhtml内にtokenを入れて、その値を使う方法です。
修正が必要な箇所は以下の3箇所です。
- Django
- html(template)
- javascript
Djangoの対応
まずは、テンプレートから使用できるようにtagを作成します。
作成する場所は、プロジェクトディレクトリがある場所にtemplate_tags/tags.py
というファイルを作成しました。
@register.simple_tag(takes_context=True) def cookie(context, cookie_name): # could feed in additional argument to use as default value request = context['request'] result = request.COOKIES.get(cookie_name, '') # I use blank as default value return result
次にsettings.pyを編集します。
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], 'libraries': { # ←これを追加 'tags': 'template_tags.tags', } }, }, ]
tagのファイルを読み込むようにしています。
これでDjango側の対応は終わりです。
html(template)の対応
Djangoで使用するtemplate html内に以下のように書きます。
<meta name="csrf" content="{% cookie 'csrftoken' %}">
最終的にhtmlのmetaタグにcsrfを追加して、jsで読み込んで使用します。
<meta name="csrf" content="W3ni7NdZeaPWYgfoRShxFVNLHCimZWxpZsL8QMJ4VtlBguv9lo7NoYGO7Gl0eD0D">
javascriptの対応
javascriptの対応も簡単です。
function call(callback) { $.ajax({ url:'/api/hoge', type:'POST', beforeSend: function(request) { let csrftoken = $("meta[name='csrf']").attr('content') request.setRequestHeader("X-CSRFToken", csrftoken); }, data:{ 'limit': 100, } }) .done(function(data) { console.log(data); callback(data); }) .fail(function(data) { console.log(data); }) }
この部分を追加すると対応が可能です。
beforeSend: function(request) { let csrftoken = $("meta[name='csrf']").attr('content') request.setRequestHeader("X-CSRFToken", csrftoken); },
また、状況に合わせて以下のコードを入れると良いとおもいます。
csrfが必要なリクエストと、不要なリクエストを分けて、必要ならcsrftokenありでリクエストするものです。
function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } function ajaxInit() { $.ajaxSetup({ beforeSend: function(xhr, settings) { let csrftoken = getCookie('csrftoken'); if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } }); }
tagを使う方法はstackoverflowで見つけました。
python - Django read cookie in template tag - Stack Overflow
失敗パターン
csrf_tokenで対応しようとして失敗
Djangoではformでリクエストを実行する際に{% csrf_token %}
というものを使えます。
これを使えばtoken値を上記みたいなコードを使わずに簡単に実装できるのではと考えました。
metaタグにcsrf_tokenを入れれると思ったのですが…
<meta name="csrf" content="{% csrf_token %}">
{% csrf_token %}
が展開されると<input>
タグが作成されるのでダメでした。
画面上に/>
という文字列が表示されていたり、domがめちゃくちゃになったり(body直下にheadがあった)しました。
あえなく断念しました。
■
DjangoでSerializerを使ったバリデーション方法について
DjangoではRestApiを作成するための便利なライブラリとしてDjango REST framework
というものがあります。
このライブラリを使うと、簡単にapiを作成することができます。
この記事ではDjango REST framework
で用意されているserializer
を使ったバリデーション方法を紹介します。
また、Django REST framework
でviewを表示する方法についても紹介します。
views.py
views.py
ではviewsetを使って実装しています。
action
を使って、get
やpost
といったメソッドを作らないようにしました。
action
を使った場合はurls
を少し工夫する必要がありますが、それについてはurls.pyを見てください。
from django.http import JsonResponse from django.shortcuts import render # Create your views here. from rest_framework import viewsets from rest_framework.decorators import action from apps.xxxxxxx.serializer import SampleSerializer class SampleViewSet(viewsets.ModelViewSet): serializer_class = SampleSerializer @action(detail=False, name='sample_post_api') def sample_post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if not serializer.is_valid(): return JsonResponse({"status": 400}, status=404) return JsonResponse(serializer.data, status=200) @action(detail=False, name='sample_get_open_view') def sample_get(self, request, *args, **kwargs): return render(request, 'home.html', status=200)
saple_get
ではrenderというショートカットを使用してhome.html
を表示しています。
restframework
のサンプルでは以下のように実装されているので、以下の方法でも良いです。
@action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer]) def highlight(self, request, *args, **kwargs): snippet = self.get_object() return Response(snippet.highlighted)
serializer.py
countというフィールドを持っています。
countは-1か1の値を受け付けます。
IntegerField
ではmin_value
とmax_value
を使えますが、0は許可してほしくありません。
そういった場合はvalidation_xxxx
というようにバリデーションを追加できます。
from rest_framework import serializers class SampleSerializer(serializers.Serializer): count = serializers.IntegerField(min_value=-1, max_value=1) def validate_count(self, count): if count == 0: raise serializers.ValidationError("count is not allowed zero.") return count
新しいシリアライザクラスを作成したほうが良いのかもしれません。
urls.py
views.py
で作成したapiを登録します。
restframework
ではrouterというものを使うのが一般的だと思いますが、この記事ではurlpatternsをそのまま使ってます。
routerについてはこちらを参照してください。
https://www.django-rest-framework.org/api-guide/routers/
また、検索するとよく見つかるurl()
というのの代わりにpath
とre_path
を使用しています。
理由はurlが廃止される可能性があるからです。
from django.contrib import admin from django.urls import path, re_path from apps.xxxxxxxx.views import SampleViewSet # admin urlpatterns = [ path('admin/', admin.site.urls), ] # sample api and view urlpatterns += { re_path(r'sample/?$', SampleViewSet.as_view(actions={ 'get': 'sample_get', })), path('api/sample/post', SampleViewSet.as_view(actions={ 'post': 'sample_post' })), }
.as_view
の’action
を使って登録します。
見るだけでわかると思います。
getでリクエストが来たら、sample_get
メソッドへ。
postでリクエストが来たら、sample_post
メソッドへ。
ちなみに、以下のように実装しないのには理由があります。
urlpatterns += { re_path(r'sample/?$', SampleViewSet.as_view(actions={ 'get': 'sample_get', 'post': 'sample_post' })), }
このようにすると、/sample
にリクエストした際に、restframework
が用意している管理画面を表示できません。
postをテストしたいのに、getが最初に表示されるので、管理画面が表示されないのです。
ハロウィンだしネタアプリを作ってみた話
タイトルの通り、ネタアプリを作ってみました。
作ったアプリケーションはこちらです。
「進捗どうですか?」というアプリケーションで、作成時間は1~2時間ぐらいです。
使用したフレームワークや環境を箇条書きにするとこんな感じです。
- Django2.1.2
- heroku free
- python3.6.4
- heroku postgresql
- domain 1円
これまで書いてきた記事を参照しつつ短時間で作ることができました。
ブログを書き続けてよかったなと感じた瞬間です。
あと、webサービスを作りたい人や、プログラミングの勉強をしている人は今回私が作ったような小さなアプリケーションから始めると良いと思います。
小さなアプリケーションでも、大きなアプリケーションでもやることは変わらないからです。
設計して、開発して、テストして、デプロイして、、あれこれと、リリースまでにやることが体験できます。
最初は時間がかかりますが、何度かやっているとやり方が分かるので、3回ぐらいやるとパパっと作れるようになります。
そして、慣れたら大きなサービスを作ってみましょう。
きっとうまくいきます