Python(会員制Webサイト)

Cookie(クッキー)について

WebブラウザとWebサーバの間で行われHTTP通信は、基本的にステートレスです。
ステートレスな通信とは、同じURLへのアクセスに対して、同じデータが返される通信を指し、以前どのようなデータをやり取りしたのかという情報は保持されません。
これだけでは、会員制のサイトを実現できないので、Webブラウザ側にクッキー(Cookie)の仕組みが追加されました。これは、Webブラウザを通して、サイト訪問者のコンピュータに一時的なデータを書き込んで保存するための仕組みです。
しかし、クッキーに何でも保存できるわけではなく、1つのクッキーに保存できる最大のデータは4096バイトに制限されています。また、クッキーは、HTTP通信のヘッダを介して入出力されることになっており、訪問者側で容易に改変が可能です。

クッキーを使ったアクセスカウンタ

#!/usr/bin/env python3
# クッキーで訪問回数のカウントアップ

import os
import cgi
from http import cookies
import datetime

# Cookieの取得
cookie = cookies.SimpleCookie(os.environ.get("HTTP_COOKIE",""))
cnt = 1
if "counter" in cookie:
    cnt = int(cookie["counter"].value) + 1

# Cookieの設定
cookie["counter"] = cnt
# 有効期限の設定
expires = datetime.datetime.now() + datetime.timedelta(days=90)
cookie["counter"]["expires"] = expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT")

# ヘッダを出力する
print("Content-Type: text/html; charset=utf-8")
print(cookie.output())
print("")
print("訪問回数=",cnt)

datetime モジュール
  • datetime.date.today()メソッド
    今日の日付を得る
  • strftime.now()メソッド
    現在の時刻を取得して一定の書式で日時を出力

    書式 意味
    %Y 西暦(4桁)
    %m 月(2桁)
    %b 月名を短縮形(Jan,Feb,Mar,...Dec)
    %d 日にち(2桁)
    %H 24時間表記で時(2桁)
    %M 分(2桁)
    %S 秒(2桁)
    %p AM/PM(2桁)
    %I 12時間表記で時(2桁)
    %w 曜日を表す数字(0:日曜,1:月曜,2:火曜,...6:土曜)
    %a 曜日名を短縮形(Sun,Mon,Tue,...Sat)
    %% 文字'%'
Session(セッション)について

セッションはクッキーを使ってデータを保存するのは同じですが、クッキーに保存するのは、訪問者に適当に付与する固有のIDだけで、実際のデータはWebサーバ側に保存します。サーバ側にデータを保存するので、クッキーの制限である保存データサイズの制限を気にする必要がありません。
また、HTTP通信はステートレスですが、セッションを使った通信では、クッキーに記録した固有のID(セッションID)をキーとして、以前のデータの値を復元する処理を行います。

セッションを使ったアクセスカウンタ

#!/usr/bin/env python3
# クッキーを使ったセッション

from http import cookies
import os,json
import datetime,random,hashlib
import cgitb

class CookieSession:
    """クッキーを使ったセッションのクラス"""
    SESSION_ID = "CookieSessionId"
    # セッションデータの保存先を指定 os.path.dirname()でパスのディレクトリ名を取得
    SESSION_DIR = os.path.dirname(os.path.abspath(__file__)) + "/SESSION"

    def __init__(self):
        # セッションデータの保存パスを確認
        if not os.path.exists(self.SESSION_DIR):
            os.mkdir(self.SESSION_DIR)

        # クッキーからセッションIDを得る
        rc = os.environ.get("HTTP_COOKIE","")
        self.cookie = cookies.SimpleCookie(rc)
        if self.SESSION_ID in self.cookie:
            self.sid = self.cookie[self.SESSION_ID].value
        else:
            # 初回の訪問ならセッションIDを生成する
            self.sid = self.gen_sid()

        # 保存してあるデータを読み出す
        self.modified = False
        self.values = {}
        path = self.SESSION_DIR + "/" + self.sid
        if os.path.exists(path):
            with open(path,"r",encoding="utf-8") as f:
                a_json = f.read()
                # JSON形式のデータを復元
                self.values = json.loads(a_json)

    def gen_sid(self):
        """セッションIDを生成する"""
        token = ":#sa$2jAiN"
        now = datetime.datetime.now().strftime("%Y%m%d%H%M%S%f")
        rnd = random.randint(0,100000)
        key = (token + now + str(rnd)).encode("utf-8")
        sid = hashlib.sha256(key).hexdigest()
        return sid

    def output(self):
        """クッキーヘッダを書き出す"""
        self.cookie[self.SESSION_ID] = self.sid
        self.save_data()
        return self.cookie.output()

    def save_data(self):
        """セッションデータをファイルに書き出す"""
        if not self.modified:
            return
        path = self.SESSION_DIR + "/" + self.sid
        # JSON形式に変換して保存
        a_json = json.dumps(self.values)
        with open(path,"w",encoding="utf-8") as f:
            f.write(a_json)

    # 添字アクセスのための特殊メソッドの定義
    def __getitem__(self,key):
        return self.values[key]

    def __setitem__(self,key,value):
        self.modified = True
        self.values[key] = value

    def __contains__(self,key):
        return key in self.values

    def clear(self):
        self.values = {}

if __name__ == "__main__":
    cgitb.enable()
    # 実行テスト(訪問カウンタの例)
    ck = CookieSession()
    counter = 1    
    if "counter" in ck:
        counter = int(ck["counter"]) + 1
    ck["counter"] = counter
    print("Content-Type: text/html; charset=utf-8")
    print(ck.output())
    print("")
    print("counter=",counter)
Webブラウザのクッキーには、カウンターの値は保持されず、データはWebサーバ側に保存されるので、訪問者自身により、セッション内の変数の値を変更することは不可能です。
特殊変数__file__
プログラム自身のパスを表す。
os.path.dirname()メソッド
パスのディレクトリ名を取得できる。
json.loads()メソッド
訪問者がWebサーバにアクセスした時、最初の訪問であれば、クッキーにセッションIDを記録し、Webサーバ内部に、セッションIDと同名のデータファイルを用意して保存したい変数の値を保存します。
保存形式はJSON形式なので、json.loads()メソッドで、Pythonのデータに変換します。
hashlib.sha256(key).hexdigest()メソッド
セッションIDは、訪問者がセッションIDを偽装して他人に成りすます"セッションハイジャック"が起こらないように、他人から類推されることのないユニークな値にする必要があります。
今回は、現在時刻とランダムな値をかけあわせたものをSHA256でハッシュ化しています。


会員制のメッセージボード

#!/usr/bin/env python3
# 会員制のメッセージボード

import os,cgi,cgitb,html
import cksession  # 自作セッションモジュール
import datetime

class SecBoard:
    """秘密のメッセージボードを実現するクラス"""

    # ユーザー名とパスワード
    USERS = { "taro":"aaa", "jiro":"bbb" }
    # メッセージのファイル
    FILE_MSG = "sec-msg.bin"

    def __init__(self):
        # URLパラメータの取得
        self.form = cgi.FieldStorage()
        self.session = cksession.CookieSession()
        self.check_mode()

    def check_mode(self):
        mode = self.form.getvalue("mode","login")
        if mode == "login":
            self.mode_login()
        elif mode == "trylogin":
            self.mode_trylogin()
        elif mode == "logout":
            self.mode_logout()
        elif mode == "sec":
            self.mode_sec()
        elif mode == "secedit":
            self.mode_secedit()
        else:
            self.mode_login()

    def print_html(self,title,html,headers = []):
        """ヘッダおよびHTMLを出力する"""
        print("Content-Type: text/html; charset=utf-8")
        for hd in headers:
            print(hd)
        print("")
        print("""
        <html><head><meta charset="utf-8">
        <title>{0}</title></head><body>
        <h2>{0}</h2><div>{1}</div></body></html>
        """.format(title,html))

    def show_error(self,msg):
        """エラーを表示"""
        self.print_html("エラー","""
        <div style="color:red">{0}</div>
        """.format(msg))

    def mode_login(self):
        """ログイン画面を表示する"""
        self.print_html("会員専用ログイン","""
        <form method="POST">
        ユーザー名: <input type="text" name="user" size="8"><br>
        パスワード: <input type="password" name="pw" size="8">
        <input type="submit" value="ログイン">
        <input type="hidden" name="mode" value="trylogin">
        </form>
        """)

    def mode_trylogin(self):
        """ログイン可能か検証する"""
        # フォームデータからログイン情報を得る
        user = self.form.getvalue("user","")
        pw = self.form.getvalue("pw","")
        # ログインできるか調べる
        if not (user in self.USERS):
            self.show_error("ユーザーが存在しません")
            return
        if self.USERS[user] != pw:
            self.show_error("パスワードが異なります")
            return
        # ログイン成功を明示
        now = datetime.datetime.now()
        self.session["login"] = now.timestamp()
        headers = [self.session.output()]
        self.print_html("ログイン成功","""
        <a href="sec-board.py?mode=sec">会員専用ボードを見る</a>
        """,headers)

    def mode_logout(self):
        """ログアウトする"""
        self.session["login"] = 0
        self.print_html("ログアウト","""
        <a href="sec-board.py">ログアウトしました</a>
        """,[self.session.output()])

    def is_login(self):
        """ログインしているか判定する"""
        if "login" in self.session:
            if self.session["login"] > 0:
                return True
        return False

    def mode_sec(self):
        """秘密のメッセージを表示する"""
        if not self.is_login():
            self.show_error("ログインが必要です")
            return
        # 秘密のメッセージを読み込む
        msg = "ここに秘密のメッセージを書いてください"
        if os.path.exists(self.FILE_MSG):
            with open(self.FILE_MSG,"r",encoding="utf-8") as f:
                msg = f.read()
        msg = html.escape(msg)
        self.print_html("秘密のメッセージ","""
        <form method="POST" action="sec-board.py">
        <textarea name="msg" rows="5" cols="80">{0}</textarea>
        <br><input type="submit" value="変更">
        <input type="hidden" name="mode" value="secedit"></form>
        <hr><a href="sec-board.py?mode=logout">→ログアウト</a>
        """.format(msg))

    def mode_secedit(self):
        """秘密のメッセージを編集する"""
        if not self.is_login():
            self.show_error("ログインが必要です","")
            return
        # 秘密のメッセージを保存
        msg = self.form.getvalue("msg","")
        with open(self.FILE_MSG,"w",encoding="utf-8") as f:
            f.write(msg)
        # 変更した旨を表示
        self.print_html("変更しました","""
        <a href="sec-board.py?mode=sec">内容を確認する</a>
        """)

if __name__ == "__main__":
    cgitb.enable()
    app = SecBoard()







改良ポイント

1.ユーザー名とパスワードがプログラム内で決め打ちになっているので、新規ユーザーを追加する仕組みがあると良いです。

2.パスワードは暗号化して、セキュリティを高めたいと思います。

3.今回のメッセージボードはすべてのユーザーで同じものを表示していましたが、セッションにログイン中のユーザー名を保存することで、固有のメッセージを保存することができるようになります。

4.使わなくなった古いセッションデータを削除する処理がなく、このままではセッションデータがWebサーバ内に増え続けてしまうので、定期的に古いセッションデータを削除する仕組みが必要です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください