Drkcore

27 02 2013 Python Flask Tweet

Flaskでセッションが入れ替わるというバグに悩まされた

13.02.27

バグの発生箇所を掴んだ。OpenIDサーバーだった。開発環境ではエラーが出なくて本番環境でのみ出現するのでサーバーを疑っていたのだけど違った。

本番環境でデバッグログ出したら

Error attempting to use stored discovery information: Claimed ID does not match (different subjects!)
Expected <wrong-openid> got <right-openid>
Attempting discovery to verify endopoint
Performing discovery on <wrong-openid>

というメッセージが出ていた。

OpenIDサーバーから戻ってきた所でおかしくなっているようだ。referrer出力するようにしたらOpenIDサーバーが認証して戻すところで入れ替わっていた。

なんでそうなるのかワケがわからないが、OpenIDサーバーを再起動したらとりあえずは治った。ploneでも似たような症状を見つけたが、Flaskのせいではないということが分かったので一安心。

仕様をきちんと押さえておくか、本を読んでおくかしておいたほうがいいけど、とりあえずはいいや。

ProductName OpenID: the Definitive Guide
David Recordon
Oreilly & Associates Inc / ?円 ( 2012-04-30 )


13.02.26

初回のログイン時じゃなくて、クッキーがエクスパイアしたあとにアクセスしてもセッションの入れ替わりが起こるみたいだ。スレッドかなぁと思ったけど、帰りの車のなかでよく考えてみたらやっぱ違うよなぁと思った。

セッションのデータがサーバー側に残っていてクッキーが送られないときにそのデータが使われているんじゃないかなぁ。

sessionをよく読んでみよう。

13.02.25

なおってなかった。

open-idサーバーで認証するところまではちゃんとユーザーが認識されているんだけど、アプリケーションにリダイレクトされたところで、ユーザーのセッションが違うユーザーのものに入れ替わってしまうというパターン。

  • バージョン0.8でも0.9でも起きる
  • 開発環境では起きないが、本番環境(Gunicorn)で起きる
  • IE8でのみ確認されている

多分ブラウザは関係ないと思う。

@app.before_requestにセッションが残っているっぽいので@oid.loginhandlerにアクセスする時に@app.before_requestのユーザーをチェックする処理をとばしてログイン処理をすればいいような気がしてきたのであとでやってみる。

  • Flask-OpenID
  • Flask-Login

Flask-OpenIDでログインするとなぜか初回に別アカウントになってしまい、一度ログアウトしてから再ログインすると正常にログインできるというよくわからないバグが頻発していて悩んでいた。しかもdevelopmentでは全く再現できず、production(Flask+Gunicorn+Supervisord)でしか起こらないので厄介だった。

Flaskのバージョンを0.8から0.9にあげてみたけどなおらなかったので、調べたら既に報告されていた。

  • Weird user cookie switching problem?
  • Session swapping issue on Apache

最初のスレッドは、「ユーザー付きあわせて違っていたらセッション破棄するぜ」みたいな解決方法だったのでちょっと使えなかったが、二番目のスレッドでsessionインターフェースを自前で用意したらなおったという報告があったので試してみた。

Flask-OpenIDでこのスニペットをそのまま利用するとopenid.yadis.manager.YadisServiceManagerがJSONシリアライズできないというエラーが出た。これはpython-openidに由来する問題らしく、コメントで指摘されているようにpickleでdumpすればsave_sessionはできるようになる。

しかし、今度はopen_sessionでコケる。managerがstrだ云々っていうエラーなのでpickle.loadsがうまくいってないようだ。Flask-OpenIDのソース読むとSessionWrapperで' p'っていうkeyのDictionaryを作っているので、Sessionのほうもそのようにしてみた。

Better Client-side sessionsをちょっと変えたらうまくいった。

from werkzeug.datastructures import CallbackDict
from flask.sessions import SessionInterface, SessionMixin
from itsdangerous import URLSafeTimedSerializer, BadSignature
import pickle

class ItsdangerousSession(CallbackDict, SessionMixin):

    def __init__(self, initial=None):
        def on_update(self):
            self.modified = True
        CallbackDict.__init__(self, initial, on_update)
        self.modified = False

class ItsdangerousSessionInterface(SessionInterface):
    salt = 'cookie-session'
    session_class = ItsdangerousSession

    def get_serializer(self, app):
        if not app.secret_key:
            return None
        return URLSafeTimedSerializer(app.secret_key,
                                      salt=self.salt)

    def open_session(self, app, request):
        s = self.get_serializer(app)
        if s is None:
            return None
        val = request.cookies.get(app.session_cookie_name)
        if not val:
            return self.session_class()
        max_age = app.permanent_session_lifetime.total_seconds()
        try:
            data = s.loads(val, max_age=max_age)
            data = data[' p']
            for k in data:
                if k.startswith('yoc') or k.startswith('lt')\
                                       or k.startswith('_openid_consumer_last_token')\
                                       or k.startswith('_yadis_services__openid_consumer_'):
                    data[k] = pickle.loads(data[k])
            return self.session_class(data)
        except BadSignature:
            return self.session_class()

    def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)
        if not session:
            if session.modified:
                response.delete_cookie(app.session_cookie_name,
                                   domain=domain)
            return
        expires = self.get_expiration_time(app, session)
        d = {}
        for k in session:
            if k.startswith('yoc') or k.startswith('lt')\
                                   or k.startswith('_openid_consumer_last_token')\
                                   or k.startswith('_yadis_services__openid_consumer_'):
                d[k] = pickle.dumps(session[k])
            else:
                d[k] = session[k]
        val = self.get_serializer(app).dumps({' p': d})
        response.set_cookie(app.session_cookie_name, val,
                            expires=expires, httponly=True,
                            domain=domain)

About

  • もう5年目(wishlistありマス♡)
  • 最近はPythonとDeepLearning
  • 日本酒自粛中
  • ドラムンベースからミニマルまで
  • ポケモンGOゆるめ

Tag

Python Deep Learning javascript chemoinformatics Emacs sake and more...

Ad

© kzfm 2003-2021