ハマった状況
Flask-Loginを使用した簡単なアプリケーションを作成中。
ブラウザから操作した範囲ではOKであることを確認した。
テストを自動化するために、requestsパッケージを使用して、ログインを実装してみたが、
ログインに成功しているにも関わらず、その後の問合せに対してログイン状態が維持されず、
再度、ログインを要求するレスポンスが返ってくる。
最初のコーディング
作成したアプリケーションは、emailとpasswordで認証する。
import requests
from bs4 import BeautifulSoup
# GET /loginでformを取得して、csrfトークンを抽出
r = requests.get("http://127.0.0.1:5000/login")
doc = BeautifulSoup(r.text, 'html.parser')
csrf_token = doc.find(attrs={'name':'csrf_token'}).get('value')
# credentialとsession cookieを指定して、POST /login
cookies = {"session": r.cookies["session"]}
credential = {"email": "user1@email.com", "password": "password1",
"csrf_token": csrf_token}
r = requests.post("http://127.0.0.1:5000/login", cookies=cookies,
data=credential, allow_redirects=True)
しかしながら、この後の呼び出しでは、ログイン状態が維持されていないため、
ログイン画面に飛ばされてしまう。
コマンドラインからcurlで実行した場合は、正しくログイン状態が維持されることは確認。
curl http://127.0.0.1:5000/login -i -c cookie.txt -b cookie.txt
curl http://127.0.0.1:5000/login -i -c cookie.txt -b cookie.txt ^
-d "email=user1@email.com" -d "password=password1" ^
-d "csrf_token=・・・" -L
調査
何が違うのか色々と調べてみたが、原因分からず。
stackoverflowで、requestsのログを出力する方法を見つけて、ログを調査。
https://stackoverflow.com/questions/16337511/log-all-requests-from-the-python-requests-module
import logging
import http.client
logging.basicConfig(level=logging.DEBUG)
httpclient_logger = logging.getLogger("http.client")
def httpclient_logging_patch(level=logging.DEBUG):
"""Enable HTTPConnection debug logging to the logging framework"""
def httpclient_log(*args):
httpclient_logger.log(level, " ".join(args))
# mask the print() built-in in the http.client module to use
# logging instead
http.client.print = httpclient_log
# enable debugging
http.client.HTTPConnection.debuglevel = 1
httpclient_logging_patch()
原因
最初にrequestsがPOST /loginすると、サーバー側で認証後、レスポンスが返る。
サーバー側がredirectで返しているので、戻り値が302 Foundとなり、
遷移先を取得しようとしてrequestsがGETを発行する。
この際、レスポンスにSet-Cookieが含まれているにも関わらず、
最初のPOSTで指定したcookieをそのまま送ってしまっている。
対応方法1
allow_redirectsをFalseに変更して、
戻り値が302の場合は、cookieを再設定してGETする。
# credentialとsession cookieを指定して、POST /login
cookies = {"session": r.cookies["session"]}
credential = {"email": "user1@email.com", "password": "password1",
"csrf_token": csrf_token}
r = requests.post("http://127.0.0.1:5000/login", cookies=cookies,
data=credential, allow_redirects=False)
if r.status_code == 302:
cookies = {"session": r.cookies["session"]}
r = requests.get(r.next.url, cookies=cookies)
対応方法2
最初から素直にsessionオブジェクトを使用する。
import requests
from bs4 import BeautifulSoup
session = requests.session()
r = session.get("http://127.0.0.1:5000/login")
doc = BeautifulSoup(r.text, 'html.parser')
csrf_token = doc.find(attrs={'name':'csrf_token'}).get('value')
credential = {"email": "user1@email.com", "password": "password1",
"csrf_token": csrf_token}
r = session.post("http://127.0.0.1:5000/login",
data=credential, allow_redirects=True)
おわりに
- 常識なのかもしれませんが、答えを求めてインターネットを三日間くらい彷徨いました。