主にプログラミングに関して。Python, .NET Framework(C#), JavaScript, その他いくらか。
記事にあるサンプルやコードは要検証。使用に際しては責任を負いかねます

スポンサーサイト

                
tags:
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Python-Tornado: セキュアなセッション管理を考える

                
 前回にTornadoでセキュアなWebサーバを作るにはどうやるかをすごくざっくりと書いた。今回はセッションの管理について、MongoDBを使ってやってみる。いわゆるログインとログアウトというものだ。
 MongoDBはNoSQLのデータベースだがPythonなりJavaScriptなりC#なりを書ければ、かなり学習の敷居の低いデータベースであり、パフォーマンスも優れている。そして各ドキュメント(リレーショナルデータベースでいうところのカラム)にTTL(Time To Live)という有効時間を設定できる。有効時間を過ぎたドキュメントは自動で削除されるので、cronを用意する必要がない。これにセッションIDの管理を任せたら便利そう。


 セッション開始のための認証は通常、IDとパスワードの入力を必要とするが、TornadoではOpenIDを使うクラスが用意されているので、これから書くデモコードではGoogleからOpenIDを取ってくるようにする。デモを簡単に走らせて結果を確認できるようにするために、SSL通信もクッキーへのセキュア属性の付与もここでは行わないが、本来はSSL通信をおこない、クッキーのセキュア属性も付与しておくべきだ。

 ログインとログアウトの機能を考える。
 ログインは認証のための情報(今回はGoogleから拝借するID)と引き換えにセッションIDを発行し、サーバ側ではセッションIDとGoogleから得たIDを紐づけてDBに保存しておく。サーバはクライアントのクッキーにセッションIDをセットし、以降は有効なセッションIDを持っているかで認証済みかどうかを判断する。セッションIDは永続化してはならないのでDBに保存されたセッションIDとGoogleIDを紐づけたドキュメント(レコード)に期限を設ける。クッキーにも有効期限は設定できるがクッキーはクライアントが改変できるものなので、サーバ側で仕組みを実装しておく。
 ログアウトは以下を読んで考えた。
ログアウト機能の目的と実現方法
[セキュリティ] ログアウト時にセッションIDのCookieを破棄する必要はあるか?について
PHPのセッション管理ライブラリがどうなってるか詳しくないので、Python+Tornadoを使う今回の状況において必要なことを書く。ログアウトでサーバの持っているセッションIDが破棄されれば、クライアントがクッキーに持つセッションIDをそれと同時に破棄する必要はない。もし破棄しなかったクライアント側のセッションIDが再び有効になるようなことがあるならそれ脆弱性。セッションIDの生成方法を修正しなければならない。UUIDの使用を考える。

 以上のことを考えてTornadoでのセッション管理を実装してみる。デモなのでセッションの有効期限が60秒間とすごく短くしてある。またTornadoは認証管理のためにRequestHandlerクラスを継承してget_current_userメソッドをオーバライドし、authenticatedデコレータをつけるという方法を用意している。今回は非同期処理が入るのでそれを使えず、自前でlogin_requiredメソッドを用意した。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import uuid
import datetime

import tornado.auth
import tornado.ioloop
import tornado.web
import tornado.gen
import tornado.httpserver

import pymongo
import motor


############################################# admin

class GoogleLoginHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
@tornado.gen.coroutine
def get(self):
if self.get_argument("openid.mode", None):
user = yield self.get_authenticated_user()
sid = str(uuid.uuid4())
yield db.session.insert({"_id":sid, "user":user["email"], "createdAt":datetime.datetime.utcnow()})
self.set_secure_cookie("sid", sid)
self.redirect(r"/admin")
else:
yield self.authenticate_redirect()

class BaseHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def login_required(self):
sid = self.get_secure_cookie("sid")
if not sid:
raise tornado.web.HTTPError(401)
doc = yield db.session.find_one({"_id":sid})
if doc:
raise tornado.gen.Return(doc["user"])
else:
raise tornado.web.HTTPError(401, "session has expired")

class ManageHandler(BaseHandler):
@tornado.gen.coroutine
def get(self):
user = yield self.login_required()
self.write("authenticated: " + user)

class LogoutHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
sid = self.get_secure_cookie("sid")
result = yield db.session.remove({"_id":sid})
self.write("bye bye")

############################################# initialize and run

def init_db(host, port):
db_name = "foo_db"
c = pymongo.MongoClient(host, port)
db = c[db_name]
db.session.ensure_index("createdAt", expireAfterSeconds = 60)
c.close()

db = motor.MotorClient(host, port)[db_name]
return db

if __name__ == "__main__":
DB_HOST = "localhost"
DB_PORT = 27017
db = init_db(DB_HOST, DB_PORT)

settings = dict(cookie_secret="foooooooooooooooooooooooooooooo",
debug=True,
)

http_server = tornado.httpserver.HTTPServer(
tornado.web.Application([('/login', GoogleLoginHandler),
('/logout', LogoutHandler),
('/admin', ManageHandler),
],
**settings),
)
http_server.listen(8890)
tornado.ioloop.IOLoop.instance().start()


            

コメントの投稿

非公開コメント

プロフィール

hMatoba

Author:hMatoba
Github

最新記事
リンク
作ったものなど
月別アーカイブ
カテゴリ
タグリスト

検索フォーム
Amazon
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。