utamaro’s blog

誰かの役に立つ情報を発信するブログ

pythonでjwtを使う際に作ったutilについて

作成したjwtのutilを紹介します。

作成したutilですが、まだまだ修正途中のものです。使用する場合は注意してください。

jwtについての詳細についてはこちらを参照してください。

https://jwt.io/introduction/

コードと解説の距離が離れると読みづらいと思いますので、できる限りコメントを書きました。

そちらを見てください。

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の対応

まずは、テンプレートから使用できるように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を使って、getpostといったメソッドを作らないようにしました。

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_valuemax_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()というのの代わりにpathre_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が最初に表示されるので、管理画面が表示されないのです。

ハロウィンだしネタアプリを作ってみた話

タイトルの通り、ネタアプリを作ってみました。

作ったアプリケーションはこちらです。

http://www.everyday.work/

「進捗どうですか?」というアプリケーションで、作成時間は1~2時間ぐらいです。

f:id:miyaji-y26:20181101082657p:plain

使用したフレームワークや環境を箇条書きにするとこんな感じです。

  • Django2.1.2
  • heroku free
  • python3.6.4
  • heroku postgresql
  • domain 1円

これまで書いてきた記事を参照しつつ短時間で作ることができました。

ブログを書き続けてよかったなと感じた瞬間です。

あと、webサービスを作りたい人や、プログラミングの勉強をしている人は今回私が作ったような小さなアプリケーションから始めると良いと思います。

小さなアプリケーションでも、大きなアプリケーションでもやることは変わらないからです。

設計して、開発して、テストして、デプロイして、、あれこれと、リリースまでにやることが体験できます。

最初は時間がかかりますが、何度かやっているとやり方が分かるので、3回ぐらいやるとパパっと作れるようになります。

そして、慣れたら大きなサービスを作ってみましょう。

きっとうまくいきます