🔐 FastAPI 実践入門:第五歩目で学ぶ認証・認可とセキュリティ対策
🧭 はじめに
FastAPIの基本とCRUD、RESTful API設計について学んだ後、次に重要なのは「セキュリティ」です。Webアプリケーションにおいては、ユーザー情報の保護、認証(Authentication)、認可(Authorization)は避けて通れない重要な要素です。
この記事では、FastAPIにおけるセキュアなAPI構築の基礎として、パスワードの暗号化、トークンベースの認証(JWT)、ログイン保護、ユーザーロールによるアクセス制限などを扱います。また、トークンの有効期限、エラーハンドリング、セキュリティ関連のベストプラクティスについても解説します。
🛡️ パスワードの安全な保存方法
パスワードをそのまま保存するのは極めて危険です。安全な方法としては「暗号化」ではなく、「ハッシュ化」を用いることが推奨されます。ハッシュ化されたパスワードは元に戻すことができず、漏洩時のリスクを大幅に下げることができます。
🔧 インストール
pip install passlib[bcrypt]
🧪 ハッシュ化の実装
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
これにより、ユーザー登録時には hash_password() を使って安全に保存し、ログイン時には verify_password() を使って入力されたパスワードと照合できます。
🔑 JWT(JSON Web Token)を使った認証
ユーザーがログインした後、トークン(JWT)を発行して、それをもとにアクセス制限を行います。JWTは署名付きの情報で構成されており、トークン自体に必要なユーザー情報や有効期限を含めることができます。
📦 インストール
pip install python-jose[cryptography]
🔐 トークンの生成と検証
from jose import JWTError, jwt
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
この create_access_token() 関数は、ユーザーIDなどの情報を含むセキュアなトークンを作成します。有効期限を設定することで、不正利用のリスクも減少します。
🚪 ログインエンドポイントの作成
以下のコードでは、OAuth2の仕組みを使って /token エンドポイントに対するPOSTリクエストでログイン認証を行い、JWTトークンを返します。
from fastapi import Depends, HTTPException, status, APIRouter
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
router = APIRouter()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
@router.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
このルートを通じて、クライアントはユーザー名とパスワードを送信し、JWTを取得します。
🔍 認証付きルートの保護
発行したJWTを使って、APIルートにアクセス制限を設けます。
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
return username
except JWTError:
raise credentials_exception
@app.get("/users/me")
def read_users_me(current_user: str = Depends(get_current_user)):
return {"username": current_user}
このようにして、保護されたルートに対しては有効なトークンを要求し、不正なアクセスをブロックできます。
🔒 ユーザーロールとアクセス制限
より高度な認可機能として、ユーザーにロール(役割)を割り当てることができます。たとえば、管理者(admin)だけがアクセスできるルートを定義することが可能です。
def get_current_active_admin(current_user: User = Depends(get_current_user)):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Not enough privileges")
return current_user
@app.get("/admin")
def read_admin_data(current_user: User = Depends(get_current_active_admin)):
return {"admin_data": "Secret info"}
ユーザーのロールはデータベースに保存し、認証時に読み込んで検証します。こうした設計により、柔軟な権限管理が可能になります。
🧪 セキュリティにおけるテストとベストプラクティス
- テスト環境では実際のSECRET_KEYを使用せず、モックを活用する
- ログイン試行制限(レートリミット)を導入してブルートフォース攻撃を防止
- HTTPS環境でのみトークンを扱うようにする
- セキュリティヘッダー(CSP, X-Frame-Optionsなど)を追加する
これらを取り入れることで、より安全なFastAPIアプリケーションを構築できます。
🎯 まとめ
- パスワードは必ずハッシュ化して保存し、プレーンテキストでは絶対に扱わない
- JWTを使って安全なトークンベース認証を構築し、セッション管理を簡素化
- FastAPIの Depends() を活用してセキュリティチェックをモジュール化・自動化
- ユーザーロールを用いたアクセス制限により、柔軟な認可ロジックを構築
- セキュリティのテストやベストプラクティスの導入で、堅牢なアプリケーションを目指す
株式会社ONE WEDGE
【Serverlessで世の中をもっと楽しく】 ONE WEDGEはServerlessシステム開発を中核技術としてWeb系システム開発、AWS/GCPを利用した業務システム・サービス開発、PWAを用いたモバイル開発、Alexaスキル開発など、元気と技術力を武器にお客様に真摯に向き合う価値創造企業です。
Discussion