conta's diary

思ったこと、やったことを書いてます。 twitter: @conta_

PythonのTornadoで解説入れながらLoginしてみる

今流行の(たぶん)PythonのWeb frameworkであるTornado。
Facebookの中の人が作ってるらしい。Instagramでも使ってるおーって、なんかの記事で見た。
概要をまとめると、ノンブロッキングでイベントル〜プな比較的シンプルに書かれた軽量で爆速なフレームワークでC10Kファイヤー(まとめ適当!)、らしい(・へ・)

Documentは良い感じなんだけれど、Webに使い方の情報が少ない気がする(Flaskとかと比べると)。
何回か使ってて結構ハマったというか、ソースコード読まないとこれわからんだろ!という部分も多々。

それはさておき、ウェブサービスを作るときにログインの部分は結構使うよね、
そしていつも”あれ、これどう書くんだっけ?”ってなるよね。

ということでとりあえずTornadoでLoginしてみよう!

フォルダの構成

今回の構成はこんな感じ。大体Flaskとかと同じ。

/App
  /static   (jsとかcssとかのスタティックファイル置き場)
  /templates (テンプレート置き場)
    login.html
  server.py  (Tornadoのコード)
  server.conf (TornadoのConfigファイル)

Tornadoサーバー書くときの全体の流れ

  • ハンドラーをクラスとして作成(クラスのメソッドにgetとかpostとかの処理を書いていく)
  • Applicationクラスにルーティングとハンドラーのクラスを紐付けしつつ、設定を書き込む
  • サーバー用のオプションをパースして、Portとか設定してサーバースタート

さぁ書くぞっ

server.pyのそーすこーど

とりあえず、ドン:|
読み飛ばしたあと、解説を書きます。

まずアプリケーションのクラス

こんな感じ。

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r'/', MainHandler),
            (r'/auth/login', AuthLoginHandler),
            (r'/auth/logout', AuthLogoutHandler),
        ]
        settings = dict(
            cookie_secret='gaofjawpoer940r34823842398429afadfi4iias',
            static_path=os.path.join(os.path.dirname(__file__), "static"),
            template_path=os.path.join(os.path.dirname(__file__), "templates"),
            login_url="/auth/login",
            xsrf_cookies=True,
            autoescape="xhtml_escape",
            debug=True,
            )
        tornado.web.Application.__init__(self, handlers, **settings)

まず、tornado.web.Applicationをを継承したApplicationクラスをつくる。
あとは、ルーティングのhandlerとアプリケーションのSettingを書いてセット!
handlersは (r'/', MainHandler) のように、ルーティングとその時の処理(GetとかPost)などを書いたクラスをヒモ付する。
(HandlerClassの中身は後ほど書きます。)

settingsについては下記にまとめる。

cookie_secret
セキュアクッキーの秘密の合言葉的な。
get_secure_cookie()とかを呼び出すときに使ってる。
static_path
 jsとかcssとかを入れておくフォルダの設定
(デフォルトで"static"になっているので不要だけど念のため)
例えば、staticフォルダにscript.jsを入れておくと
http://hogehoge.com/static/script.js
みたいな感じでアクセスできるようになる。
template_path
htmlテンプレートのフォルダの設定
(デフォルトで"templates"になっているので不要だけど念のため)
login_url
 ログインが必須なURLにアクセスした時に、ログインしてなかった場合に飛ばされる場所
(@tornado.web.authenticatedのデコレータがついたメソッドが呼び出された時にログインしてなかったら飛ばされる場所)
xsrf_cookies
XSRF対策用?ノリでTrueにしてみた。
Tornadoには、xsrf_form_html() というヘルパーみたいなものがあって、
それをでformタグ内に書くと自動的にcookieとフォームタグに乱数をセットしてくれるらしい。
ドキュメントには、
If you have set the ‘xsrf_cookies’ application setting, you must include this HTML within all of your HTML forms.
と書いてあるので、Trueにしておくと全部のフォームタグ内にxsrf_form_html()をつけとかないとダメってことかな?
autoescape
テンプレートエンジンを使うときに自動でエスケープするかどうかの設定
ドキュメントさんいわく、
All template output is escaped by default, using the tornado.escape.xhtml_escape function. This behavior can be changed globally by passing autoescape=None to the Application or TemplateLoader constructors, for a template file with the {% autoescape None %} directive, or for a single expression by replacing {{ ... }} with {% raw ...%}. Additionally, in each of these places the name of an alternative escaping function may be used instead of None.
だそうです。
つまり、tempate.htmlの中に出力する部分”{{...}}”のところを自動でエスケープするかどうかの設定、だと思う
debug
Trueにしておくと、ファイルを更新した時にリロードしてくれたり、
なにかエラーが起きた時にBrowser上にエラーを出力してくれると思う。

settingsの一覧ってどこに書いてあるのかわからん。

べーすはんどらー

tornado.web.RequestHandlerを継承したBaseHandlerを作る。

class BaseHandler(tornado.web.RequestHandler):

    cookie_username = "username"

    def get_current_user(self):
        username = self.get_secure_cookie(self.cookie_username)
        logging.debug('BaseHandler - username: %s' % username)
        if not username: return None
        return tornado.escape.utf8(username)

    def set_current_user(self, username):
        self.set_secure_cookie(self.cookie_username, tornado.escape.utf8(username))

    def clear_current_user(self):
        self.clear_cookie(self.cookie_username)

ログイン機能をつけるために、ここにget_current_user()というのを実装しておく。
もともとtornado.web.RequestHandlerにget_current_user()というのが準備されていて、
@tornado.web.authenticatedのデコレータをつけたメソッドは、
get_current_user()で何かしら値を返すとパスできて、
何も書いてないとlogin_urlへリダイレクトされる仕組み。
set_current_user()と、clear_current_user()は利便性を高めるための自作メソッド。

メインハンドラー

先程作ったBaseHandlerを継承。
Applicationのhandlersで、(r'/', MainHandler)と設定してあるので、
http://localhost:5000/にアクセスすると、このクラスで処理される。

class MainHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        self.write("Hello, <b>" + self.get_current_user() + "</b> <br> <a href=/auth/logout>Logout</a>")

でたっ!@tornado.web.authenticated
これをつけておくと、このハンドラーでgetされた時にログインしているかチェックされる。ログインしてなかったらリダイレクトされる。

self.write("Hello, <b>" + self.get_current_user() + "</b> <br> <a href=/auth/logout>Logout</a>")

の部分は、ログインしていれば、ユーザーネームを出力して、ログアウトのリンクが現れるように書いた。

ログインハンドラー

これもBaseHandlerを継承。
/auth/login にgetされると、login.htmlを出力。
postされるとログイン認証を行う。

login.htmlはこんな感じ。

<!DOCTYPE HTML>
<html lang="en-US">
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <h1>Login</h1>
    <form action="/auth/login" method="post">
      {% module xsrf_form_html() %}
      <p>username : <input type="text" name="username"/></p>
      <p>password : <input type="password" name="password"/></p>
      <input type="submit" value="Login!"/>
    </form>
  </body>
</html>

login.html内の{% module xsrf_form_html() %}は、render()によって、

<input type="hidden" name="_xsrf" value="8bc949dcb6ae4734a48b7a514a1ed759">

に変換される。

これがハンドラー

class AuthLoginHandler(BaseHandler):

    def get(self):
        self.render("login.html")

    def post(self):
        logging.debug("xsrf_cookie:" + self.get_argument("_xsrf", None))

        self.check_xsrf_cookie()

        username = self.get_argument("username")
        password = self.get_argument("password")

        logging.debug('AuthLoginHandler:post %s %s' % (username, password))

        if username == options.username and password == options.password:
            self.set_current_user(username)
            self.redirect("/")
        else:
            self.write_error(403)

POSTの流れは下記の通り。

  1. xsrfチェック
  2. usernameとpasswordをチェック
  3. 合ってたらクッキーにusernameをセット
  4. リダイレクト

self.check_xsrf_cookie()は、cookieにセットされた_xsrfの値とpostで受け取った値をチェックして、
ダメだったら403エラーを出力してくれるメソッド。

options.username と options.passwordについては後ほど。

ログアウトハンドラー

これだけ。
/auth/logout にアクセスがあったら、クッキー内のusernameを削除

class AuthLogoutHandler(BaseHandler):

    def get(self):
        self.clear_current_user()
        self.redirect('/')

全体像+mainのところ

import tornado.ioloop
import tornado.web
import tornado.escape
import tornado.options
from tornado.options import define, options

import os
import logging

define("port", default=5000, type=int)
define("username", default="user")
define("password", default="pass")

〜省略〜

def main():
    tornado.options.parse_config_file(os.path.join(os.path.dirname(__file__), 'server.conf'))
    tornado.options.parse_command_line()
    app = Application()
    app.listen(options.port)
    logging.debug('run on port %d in %s mode' % (options.port, options.logging))
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    main()

ここで、サーバーの起動する所を書いてます。
流れは、

  1. server.conf内の設定を読み込み
  2. コマンドラインからオプションの読み込み
  3. 作成したApplicationクラスからオブジェクト生成
  4. 設定したportでlisten
  5. サーバー起動

となる。

いきなり出てきたdefineってなんぞい??ってなりますよね。
これはコマンドラインからのoptionを追加できる便利なメソッドらしい。

そして、この部分がオプションをパースしてくれる。

tornado.options.parse_config_file(os.path.join(os.path.dirname(__file__), 'server.conf'))
tornado.options.parse_command_line()

parse_config_file()は予め設定ファイルにオプションを記述しておくと、
option.hogehogeのようにアクセスできるようになる。
今回はport, username, passwordをオリジナル設定として定義してる。
AuthLoginHandler内の、options.username と options.passwordはここで定義されてたものを使っている。

ちなみに、server.confの中身はこんな感じ。

port=5000
logging='debug'

logging='debug'はloggingの出力レベルを設定してくれる部分。
これでlogging.debug()が出力されるようになる。
Tornadoにはloggingオプションが標準で付いているっぽい。

parse_command_line()はコマンドラインに書いたoptionを適応してくれるメソッド。

python server.py --port=8888

みたいに書くとソースの変更なしにパラメータを変更できる(と思う)

いざ起動

1. http://localhost:5000にアクセス

f:id:contaconta:20120531215809p:plain

2. user, passと入力

f:id:contaconta:20120531215815p:plain

3. login!

f:id:contaconta:20120531215820p:plain

おっしゃ!でけた!

おわりに

パスワードが決め打ち&平文なのでまだ実用的ではないですが、
認証の部分にDB入れたり、shaとかで暗号化すればとりあえずOKな気がします。
ソースコードはGithubにおいてあります。
https://github.com/contaconta/TornadoAuthTest

文章長い!日本語へたい!
プログラム書くよりブログ書くほうが時間かかるお(´・ω・`)