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サーバ内に増え続けてしまうので、定期的に古いセッションデータを削除する仕組みが必要です。