Google App Engine 用のログイン認証機能を作ってみた
Google App Engine では Google アカウントの認証機能がそのまま利用できるので、ログイン管理の手間が掛からない。しかし、当然のことながらアプリケーションの利用者は Google アカウント保持者に限定されてしまうし、逆に何も制限を加えないと Google アカウント保持者は誰でもアクセスできてしまうことになる。
そこで Google アカウントとは無関係にアカウントを管理する仕掛けのひな形を作ってみた。サンプルは http://teshigoto-showcase-02.appspot.com/ で実際に稼働している。ここで新規登録を行えばアカウントを取得することができるが、試用アカウントを使えば新規登録しなくてもログインすることができる。試用アカウントはサンプルサイトのトップページに記載してある。また、ソースコードは http://www.teshigoto.net/showcase.html の「Another Loin Utility – GAE case study 2」に置いた。
アカウントの新規登録を行うとき、同一ログイン名の同時登録を避けるため、ファイルを使ったロック処理を行おうとしたが、Google App Engine では肝心の fcntl を利用できない。そこで今回は登録依頼のあったアカウントを無条件に Datastore に格納してしまい、その後改めて同一のログイン名による検索を行って複数のエンティティが見つかったときは、自分自身を削除して「同じログイン名がすでに登録されてます」メッセージを出す、という手順にしてみた。具体的には次のような感じ。
new_user = users.Users(id = new_login_id, ...) new_user.put() query = users.Users.all() query.filter('id =', new_login_id) count = 0 for user in query: count += 1 if count == 1: # regist copleted pass else: # duplicated id new_user.delete()
このやり方だと同じログイン名を同時に登録しようとして、どちらも失敗する可能性が残るが、一般的なログイン名ではそれでも構わないだろうと判断した。
ログインすればセッション管理も必要になるので、これも作ってみた。前回公開したサンプルでもセッション管理を作ったものの中途半端だったので、もう少し手を入れてみた。セッション ID を Cookie として送信するために準備する機能(送信そのものは画面表示モジュールが行うことになる)や、セッション継続時に保存しておくデータを Datasore に格納する機能などがある。
import os import re import time import random import hashlib from google.appengine.ext import db class SessionDb(db.Expando): sid = db.StringProperty() DEFAULT_SID_NAME = 'alu_001' class Session(): def __init__(self, req, res, sid_name=DEFAULT_SID_NAME): self.sid_name = sid_name self.req = req self.res = res if sid_name in req.cookies: self.sid_value = req.cookies[sid_name] else: self.sid_value = '' def new_ssn(self, ssl=False): random.seed() random_str = str(random.random()) + str(random.random()) random_str = random_str + str(time.time()) random_str = random_str + os.environ['REMOTE_ADDR'] self.sid_value = hashlib.sha256(random_str).hexdigest() cookie_val = self.sid_name + '=' + self.sid_value if ssl: cookie_val += ';secure' self.res.headers.add_header('Set-Cookie', cookie_val) ssn_db = SessionDb(sid=self.sid_value) ssn_db.put() def destroy_ssn(self): ssn_db = SessionDb.all() ssn_db.filter('sid =', self.sid_value) ssn = ssn_db.fetch(1) db.delete(ssn) expires = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", time.gmtime(0)) cookie_val = self.sid_name + '=null' + ';expires=' + expires self.res.headers.add_header('Set-Cookie', cookie_val) def get_ssn_data(self, k): ssn_db = SessionDb.all() ssn_db.filter('sid =', self.sid_value) ssn = ssn_db.fetch(1) return ssn[0]._dynamic_properties[k] def set_ssn_data(self, k, v): ssn_db = SessionDb.all() ssn_db.filter('sid =', self.sid_value) ssn = ssn_db.fetch(1) ssn[0]._dynamic_properties[k] = v ssn[0].put() def chk_ssn(self): ssn_db = SessionDb.all() ssn_db.filter('sid =', self.sid_value) count = 0 for i in ssn_db: count += 1 if count == 1: return True else: return False
さらに、今回のサンプルでも多言語対応を試みた。前回はオリジナルのちょっと安直な方法で実装したが、今回は gettext を利用してみた。これによって言語リソースを完全に Python のソースコードと分離することができた。また、表示言語を固定せずに、利用者からの指示で動的に言語を切り替えられるようにした。具体的には Python ライブラリリファレンス「6.28 gettext — 多言語対応に関する国際化サービス」を参考にしながら、おおよそ次のような流れで処理を行っている。
locale_path = os.path.dirname(__file__) + '/../locale' # リソースファイルを置いた場所 supported_lang = ['ja', 'en'] # サポートする言語の一覧 for lang_name in self.supported_lang: lang[lang_name] = gettext.translation('resource', locale_path, languages=[lang_name]) accept_lang = 'ja' # 実際の表示言語をここで指定 lang[accept_lang].install() _ = lang[accept_lang].gettext print _('message_sentence') # 指定した言語で表示される