お疲れ様です。tkmiです。
FastAPIでJWT認証を入れたあと、多くの人が次に悩むのが権限管理かと思います。
システムを使う人は増えてきて、機能も多くなってきた、けど使う人によって実行できる機能を分けたい時があると思います。
最初は簡易的なもので…そこまで厳密に…なんて思っていると
動くけど、あとから確実に辛くなる構成になりがちです。
- adminだけ実行できるAPIにする
- 一般ユーザーは閲覧のみ
- routerに「 if user.role != "admin" 」が増殖する
この記事では、
FastAPIで破綻しにくい権限管理(RBAC)を、最小構成で実装する方法 を紹介します。
認証と認可は別物
まず大前提として、この2つは別です。
- 認証(Authentication)
→「誰か?」(ログインしているか) - 認可(Authorization)
→「何をしていいか?」
FastAPIではどちらもDependsで書けてしまうため、
この2つが混ざりやすいのが落とし穴です。
この記事では、
- 認証:get_current_user
- 認可:require_role
と、役割を明確に分けます。
ディレクトリ構成(最小)
app/
├─ core/
│ ├─ security.py # 認証
│ └─ permissions.py # 認可
├─ models/
│ └─ user.py
├─ routers/
│ └─ admin.py
└─ main.py
ポイントは routerにロジックを書かない ことです。
Userモデル(最小)
from pydantic import BaseModel
class User(BaseModel):
id: int
email: str
role: str
ORMやDBはここでは省略します。
認証:現在のユーザー取得
from app.models.user import User
def get_current_user() -> User:
# 実際はJWTをデコードしてDBから取得する
return User(
id=1,
email="admin@example.com",
role="admin",
)
ここでは「認証済みユーザー」を取得するのみです。
認可:roleチェックをDependsで表現する
from fastapi import Depends, HTTPException, status
from app.core.security import get_current_user
from app.models.user import User
def require_role(roles: list[str]):
def checker(user: User = Depends(get_current_user)):
if user.role not in roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied",
)
return user
return checker
ここが重要なポイント
- routerに「if」を書かない
- 権限チェックは宣言的に書く
- Dependsは「処理」ではなく「条件」
router側の実装
from fastapi import APIRouter, Depends
from app.core.permissions import require_role
from app.models.user import User
router = APIRouter(prefix="/admin")
@router.post("/users")
def create_user(
user: User = Depends(require_role(["admin"]))
):
return {"message": "admin only action"}
このrouterを見れば、
- このAPIはadmin専用
- 権限チェックは外に出ている
ということが一瞬で分かります。
よくあるアンチパターン
- ❌ routerで「 if user.role != "admin" 」
- ❌ 認証と認可を同じ関数でやる
- ❌ JWTに権限情報を詰め込みすぎる
- ❌ APIごとに権限ロジックがバラバラ
これらは、規模が少し大きくなると一気に破綻します。
まとめ
- 認証と認可は分ける
- 権限チェックはDependsで宣言する
- routerは「何が必要か」だけを書く
最初は role-based(RBAC) で十分だと思います。
これにpermissionを加えたりして拡張するといいと思います!

