[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

Pythonで一番小さいWEBフレームワークbottle.py その6(COOKIES)

少し間が空いてしまいましたが、久々にbottleについてまとめていきます。今回はCookieあたりを見ていきます。

Cookie(クッキー)

cookie はクライアント側のブラウザの機能で、サーバから送られてきたデータについてテキスト形式で保持されています。また、同一サーバへのアクセス時はブラウザが保持しているcookie をサーバ側に送信します。

利用用途は様々ですが、通常は初回アクセス時に発行したIDをcookie に保存させることで、同一ブラウザからのアクセスを識別することに用いられます。

この動作は、会員ページのログインやEC2サイトに於ける買い物かご等の実現に必要不可欠です。

bottleでとりあえず使ってみる

bottleでcookie を使う場合はresponse.set_cookierequest.get_cookieを使用します。以下はマニュアルと同じサンプルにコメントを追加したものです。

# coding:utf-8
from bottle import (
    run,
    route,
    request,
    response
)


@route('/hello')
def hello_again():
    # cookieにvistedというKeyがあるかをチェック
    if request.get_cookie("visited"):
        return "Welcome back! Nice to see you again"
    else:
        # cookieに該当Keyがない(初回のアクセス時)はvisted:yes をcookieに書き込む
        response.set_cookie("visited", "yes")
        return "Hello there! Nice to meet you"


if __name__ == "__main__":
    # テスト用のサーバをlocalhost:8080で起動する
    # reloader=Trueにより、ソースを書き換えると自動的に再起動される
    run(host='localhost', port=8080, reloader=True, debug=True)

これをcookie.pyとして保存し、python cookie.pyとして起動するとhttp://localhost:8080/helloへの初回アクセス時はHello there! Nice to meet youと表示され、2度め以降はWelcome back! Nice to see you againが表示されます。

responseはサーバがブラウザへ戻す情報、requestはブラウザからサーバが受け取る情報です。cookieはサーバがブラウザに保存を命令するのでresponse.set_cookieであり、ブラウザはサーバへアクセス時に送信するためrequest.get_cookieで扱います。

cookieのオプション

cookieを保存させる場合は以下のオプションを追加することが可能です。

オプション 意味 デフォルト
max_age クッキーの残存時間の秒数 None
expires クッキーの有効期限のUnixタイムスタンプ None
domain クッキーを読み取れるドメイン名 current domain
path クッキーが送信されるURLパス /
secure HTTPSでのみクッキーが使用される off
httponly クライアントサイドのJSでクッキーを読み取れないようにする off
same_site クロスサイトリクエストと共にクッキーを送信しない None

たとえば、max_age=10を指定してみます。

@route('/max_age')
def max_age_test():
    # cookieにmax_age_testというKeyがあるかをチェック
    if request.get_cookie("max_age_test"):
        return "Welcome back! Nice to see you again"
    else:
        # cookieに該当Keyがない(初回のアクセス時)はmax_age_test:yes を10秒期限でcookieに書き込む
        response.set_cookie("max_age_test", "yes", max_age=10)
        return "Hello there! Nice to meet you"

この場合、2度めのアクセスでは同じようにWelcome back! Nice to see you againが表示されますが、初回アクセスから10秒経過するとcookie が削除され、再びHello there! Nice to meet youが表示されます。

Signed Cookiesについて

cookieはクライアントのブラウザ内に格納されているため、悪意のあるクライアントによって容易に偽造されます。例えば以下のコードを見ていきます。

@route('/visit_count')
def visit_count():
    """
    cookie に訪問回数を格納して表示するサイト
    :return: 
    """
    cookie_name = "visit_count"
    visited_count = 0
    if not request.get_cookie(cookie_name):
        response.set_cookie(cookie_name, '1')
        visited_count = 1
    else:
        visited_count = int(request.get_cookie(cookie_name))

    # Secret key required for non-string cookies.'というエラーになるので一旦strにしている
    response.set_cookie(cookie_name, str(visited_count + 1))

    return "あなたは {} 回目のアクセスです。".format(visited_count)

アクセスすると、これまでのアクセス回数が表示されます。

あなたは 4 回目のアクセスです。

しかし、この値はブラウザ側に保存されているため例えばChromeのデベロッパーツール等で簡単に書き換えることができます。

f:id:denzow:20171209103744p:plain

この状態でアクセスすると

あなたは 1000 回目のアクセスです。

というように値を書き換えられてしまっています。これを避けるには以下のように書き換えます。

@route('/visit_count')
def visit_count():
    """
    cookie に訪問回数を格納して表示するサイト
    :return:
    """
    cookie_name = "visit_count"
    visited_count = 0
    if not request.get_cookie(cookie_name, secret='my secret cookie'):
        # secret を指定
        response.set_cookie(cookie_name, '1', secret='my secret cookie')
        visited_count = 1
    else:
        visited_count = int(request.get_cookie(cookie_name, secret='my secret cookie'))
    # secret を指定
    response.set_cookie(cookie_name, str(visited_count + 1), secret='my secret cookie')

    return "あなたは {} 回目のアクセスです。".format(visited_count)

ほとんど内容はかわりませんがresponse.set_cookie(cookie_name, '1', secret='my secret cookie')のようにcookie 生成時にmy secret cookieを元にcookie が暗号かされてクライアント側に保存されます。

f:id:denzow:20171209103751p:plain

そのためクライアント側では意図した値に書き換えられないようになります。なお、set_cookiestr以外を渡した場合、bootleは自動でpickleをしますが、その際はsecretが必須になっています。未指定の場合はSecret key required for non-string cookies.が発生しますのでそのような値を入れる場合はsecretを必ず指定するようにします。

    def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options):
        :
        :
        elif not isinstance(value, basestring):
            raise TypeError('Secret key required for non-string cookies.')

まとめ

cookie の基本的な使い方をまとめました。直接cookie を操作するケースというのはあまりないかもしれませんがbottleでシンプルなアプリケーションを作る場合には必要になることもあるので、どなたかの一助になれば幸いです。