プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

FastAPI でセッションを扱う方法

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  92 10:37 cookie.json
-rw-r--r-- 1 mor mor    0  63 12:51 environment.json
-rw-r--r-- 1 mor mor 9243  92 10:37 history.json
drwxr-xr-x 4 mor mor 4096  63 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)