FastAPI でセッションを扱う方法を調べてみました。 SessionMiddleware というのを使えば、簡単に使うことができるようです。
FastAPIでのHTTPセッション管理について:(Google OAuth 2.0実装例付き) | 知と富の交差点
この記事にも書かれていますが、 SessionMiddleware は署名付きクッキーを使ってセッションを管理してくれるようです。
準備
まずは必要なパッケージをインストールしておきます。
pip install fastapi uvicorn python-dotenv itsdangerous
なお、itsdangerous はサンプルファイルから直接呼んでいないのですが、 SessionMiddleware を使うと内部で使っているようで、必要になります。
設定方法
SessionMiddleware を使うには、
# FastAPI アプリケーションの作成 app = FastAPI() app.add_middleware( SessionMiddleware, secret_key=_FASTAPI_SESSION_SECRET_KEY, max_age=(int)(datetime.timedelta(minutes=_SESSION_TIMEOUT_MIN).total_seconds()), )
のようにすれば OK です。ここで、 _FASTAPI_SESSION_SECRET_KEY はセッション情報の署名のための秘密鍵になります。今回の場合は、 .env ファイルから読み込むようにしました。
# 環境変数の読み込み # 秘密鍵 (セッション管理用)が必要 # .env ファイルから取得する load_dotenv() _FASTAPI_SESSION_SECRET_KEY = os.environ.get("FASTAPI_SESSION_SECRET_KEY") if not _FASTAPI_SESSION_SECRET_KEY: raise ValueError("FASTAPI_SESSION_SECRET_KEY is not set in environment variables.") _SESSION_TIMEOUT_MIN = 10 # セッション有効期限, 分単位
.env ファイルは
FASTAPI_SESSION_SECRET_KEY = "session_secret_key"
こんな感じです。
セッションの使い方
ネットのサンプルだと認証に絡んだものが多い印象があるので、ここでは何かの API にアクセスした際にセッション情報があれば、それを返し、なければ新規にセッション情報を作成するようなケースを考えます。
まず、この API は POST でアクセスすると想定して、POSTのペイロードを定義しておきます。
# POST のペイロードを定義 class PostPayload(BaseModel): question: str = Query( min_length=1, max_length=1000, description="The question to query the FAQ system.", )
このあたりは詳しくは FastAPI のドキュメントなどを見てください。
次に API を定義します。
# Depends を使った API @app.post("/query/") async def query(payload: PostPayload, session: dict = Depends(_get_session)): print(f"session is: {session}") # レスポンスの整形 result = { "session_id": session["session_id"], "count": session["count"], "query": str(payload.question), } return result
ここで、 _get_session という関数を依存性注入という仕組みを使って渡しています。この関数は、
def _get_session(request: Request): print(f"get_session, target is: {request.session}") if not request.session: request.session["session_id"] = str(uuid.uuid4()) request.session["count"] = 0 request.session["count"] += 1 return request.session
という感じに定義してあり、セッションがなければ session_id を新規に作成し、既存のセッションがあればそれを利用するという処理を行います。また、わかりやすいように、 session_id ごとの呼び出し回数をカウントするようにしています。
テスト
これでテストしてみます。一連の処理を書いたファイルの最後に、
if __name__ == "__main__": import uvicorn # Start the FastAPI application uvicorn.run(app, host="127.0.0.1", port=8000)
と追記して、 VSCode から実行します。
こちらの記事でも触れたように、 FastAPI で定義した API を呼び出してテストするには VSCode の拡張機能である REST Client を使います。test.http というファイルを作成し、
POST http://localhost:8000/query/ Content-Type: application/json { "question": "question 1" }
のように記述しておきます。この時、『Send Request』と表示されているので、クリックするとコンソールに、
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) get_session, target is: {} session is: {'session_id': '505185b0-3b07-487f-9591-ff0d56fc3390', 'count': 1} INFO: 127.0.0.1:49634 - "POST /query/ HTTP/1.1" 200 OK
のように表示されます。初回のリクエストのため、セッションがないことがわかります。また、VSCode 上でウィンドウが開いて、
のように、応答が表示されます。ここで、 set-cookie ヘッダが送られていることも確認できます。
同じAPIを2回目呼び出すと、
get_session, target is: {'session_id': '505185b0-3b07-487f-9591-ff0d56fc3390', 'count': 1} session is: {'session_id': '505185b0-3b07-487f-9591-ff0d56fc3390', 'count': 2} INFO: 127.0.0.1:42480 - "POST /query/ HTTP/1.1" 200 OK
のようになり、 session_id は同じだけど、 count が加算されていることがわかります。
なお、REST Client の cookie はどうなっているかも見てみます。REST Client の cookie などの情報は、 ~/.rest-client にあります。
mor@DESKTOP-DE7IL4F:~/.rest-client$ ls -l 合計 20 -rw-r--r-- 1 mor mor 370 9月 2 10:37 cookie.json -rw-r--r-- 1 mor mor 0 6月 3 12:51 environment.json -rw-r--r-- 1 mor mor 9243 9月 2 10:37 history.json drwxr-xr-x 4 mor mor 4096 6月 3 12:51 responses mor@DESKTOP-DE7IL4F:~/.rest-client$
さきほどの1回目のリクエスト後に cookie を表示してみると
mor@DESKTOP-DE7IL4F:~/.rest-client$ cat cookie.json {"localhost":{"/":{"session":{"key":"session","value":"eyJzZXNzaW9uX2lkIjogIjUwNTE4NWIwLTNiMDctNDg3Zi05NTkxLWZmMGQ1NmZjMzM5MCIsICJjb3VudCI6IDF9.aLZKUg.YpJ3eJiukxLAcyG3i1XIw24VNAM","maxAge":600,"domain":"localhost","path":"/","httpOnly":true,"extensions":["samesite=lax"],"hostOnly":true,"creation":"2025-09-02T01:37:22.546Z","lastAccessed":"2025-09-02T01:37:22.546Z"}}}}mor@DESKTOP-DE7IL4F:~/.rest-client$ mor@DESKTOP-DE7IL4F:~/.rest-client$
のようになっています。署名付き cookie なので当然内容はわかりませんが。
2回目のリクエストの後だと、
mor@DESKTOP-DE7IL4F:~/.rest-client$ cat cookie.json {"localhost":{"/":{"session":{"key":"session","value":"eyJzZXNzaW9uX2lkIjogIjUwNTE4NWIwLTNiMDctNDg3Zi05NTkxLWZmMGQ1NmZjMzM5MCIsICJjb3VudCI6IDJ9.aLZLVg.Hvv8fAkOWQWDsyO2eZrLEiSJNoU","maxAge":600,"domain":"localhost","path":"/","httpOnly":true,"extensions":["samesite=lax"],"hostOnly":true,"creation":"2025-09-02T01:37:22.546Z","lastAccessed":"2025-09-02T01:41:42.730Z"}}}}mor@DESKTOP-DE7IL4F:~/.rest-client$ mor@DESKTOP-DE7IL4F:~/.rest-client$
のようになっています。value の値をよく見ると、1回目とは値が異なっています。
これは、多分、セッション情報を使って署名(ハッシュ操作?)を作っているため、 count の値が変わったことで、 cookie の設定値も変化したのだと思われます。
別の API の定義方法
さて、上記の方法は依存性注入というのを使ったのですが、別に無理にこれを使わなくてもセッション情報を扱うことができます。また、 SessionMiddleware を使わず、直接 cookie を操作することでセッション情報を扱うこともできます。
とうことで、これらも簡単に書いておきます。
方法2
SesionMiddleware を使うけど、 依存性注入は使わない方法です。
下記のドキュメントにあるように、POSTのペイロードと一緒に request オブジェクトを受け取ることができるので、これを元にセッションを操作できます。
Using the Request Directly - FastAPI
たとえば、こんな感じに定義できます。
# request で受け取る API @app.post("/query2/") async def query2(payload: PostPayload, request: Request): session: dict = _get_session(request) print(f"query2, session is: {session}") # レスポンスの整形 result = { "session_id": session["session_id"], "count": session["count"], "query2": str(payload.question), } return result
このサンプルだと、 request を _get_session 関数に渡して処理していますが、 request.session を直接いろいろと操作することができます。
方法3
もっと簡単にやるには、 SessionMiddleware を使わず、直接 Cookie を操作することで実現できます。
今回、自分では試していませんが、例えば、下記の記事のようなやり方です。
🚀 FastAPI 実践入門:第三歩目で学ぶテンプレートとセッション管理
この場合、 cookie の中身を見ることができてしまうので、機密の高いものには使えないですかね。
セッションのクリア
あと、セッションのクリアをするには、最初の方法と方法2であれば、
# セッションのクリア @app.get("/clear") async def clear_session(request: Request): request.session.clear() return {"message": "Session cleared"}
のようにします。
方法3の cookie を直接使う場合は
@app.get("/delete") async def delete_session(request: Request): response = JSONResponse(content={"message": "Session cookie deleted"}) response.delete_cookie("session") return response
のように、cookieを削除する方法で実現できます。ただし、 SessionMiddleware を使う場合はこれだとうまくいきません(request オブジェクトに session が残っていると再度 set-cookie ヘッダが送られているみたいです)のでご注意を。
おまけ
今回のサンプルで使った python ファイルの全体を載せておきます。ご参考までに。
import datetime import os import uuid from dotenv import load_dotenv from fastapi import Depends, FastAPI, Query, Request from pydantic import BaseModel from starlette.middleware.sessions import SessionMiddleware from starlette.responses import JSONResponse """ FastAPI でセッションを使うためのサンプル SessionMiddleware を使って、セッションを扱う。 セッションは署名付きクッキーを使う。 署名は秘密鍵方式となる(このため、秘密鍵を指定する必要がある)。 """ # 環境変数の読み込み # 秘密鍵 (セッション管理用)が必要 # .env ファイルから取得する load_dotenv() _FASTAPI_SESSION_SECRET_KEY = os.environ.get("FASTAPI_SESSION_SECRET_KEY") if not _FASTAPI_SESSION_SECRET_KEY: raise ValueError("FASTAPI_SESSION_SECRET_KEY is not set in environment variables.") _SESSION_TIMEOUT_MIN = 10 # セッション有効期限, 分単位 # POST のペイロードを定義 class PostPayload(BaseModel): question: str = Query( min_length=1, max_length=1000, description="The question to query the FAQ system.", ) # FastAPI アプリケーションの作成 app = FastAPI() app.add_middleware( SessionMiddleware, secret_key=_FASTAPI_SESSION_SECRET_KEY, max_age=(int)(datetime.timedelta(minutes=_SESSION_TIMEOUT_MIN).total_seconds()), ) def _get_session(request: Request): print(f"get_session, target is: {request.session}") if not request.session: request.session["session_id"] = str(uuid.uuid4()) request.session["count"] = 0 request.session["count"] += 1 return request.session # 問い合わせ エンドポイント # Depends を使った API @app.post("/query/") async def query(payload: PostPayload, session: dict = Depends(_get_session)): print(f"session is: {session}") # レスポンスの整形 result = { "session_id": session["session_id"], "count": session["count"], "query": str(payload.question), } return result # request で受け取る API @app.post("/query2/") async def query2(payload: PostPayload, request: Request): session: dict = _get_session(request) print(f"query2, session is: {session}") # レスポンスの整形 result = { "session_id": session["session_id"], "count": session["count"], "query2": str(payload.question), } return result # セッションのクリア @app.get("/clear") async def clear_session(request: Request): request.session.clear() return {"message": "Session cleared"} @app.get("/delete") async def delete_session(request: Request): # これだとうまくセッションが削除できない # # たぶん、 delete_cookie でクッキーを削除するが、 # request オブジェクトに残っているセッション情報があるため、 # 自動的にセッションがもう一度設定されるためと思われる response = JSONResponse(content={"message": "Session cookie deleted"}) response.delete_cookie("session") return response if __name__ == "__main__": import uvicorn # Start the FastAPI application uvicorn.run(app, host="127.0.0.1", port=8000)