13-JWT双Token机制:access_token + refresh_token实现

53 阅读2分钟

JWT双Token机制:access_token + refresh_token实现

前言

双Token机制是现代Web应用的标准认证方案。本文介绍如何在FastAPI中实现安全的JWT双Token认证。

适合读者: 后端开发者、安全工程师


一、Token生成

# auth/jwt.py
from jose import jwt
from datetime import datetime, timedelta

SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"

def create_access_token(user_id: int, username: str) -> str:
    """生成Access Token(30分钟有效)"""
    expire = datetime.utcnow() + timedelta(minutes=30)
    payload = {
        "user_id": user_id,
        "username": username,
        "exp": expire,
        "type": "access"
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def create_refresh_token(user_id: int) -> str:
    """生成Refresh Token(7天有效)"""
    expire = datetime.utcnow() + timedelta(days=7)
    payload = {
        "user_id": user_id,
        "exp": expire,
        "type": "refresh"
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(token: str, token_type: str = "access") -> dict:
    """验证Token"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        if payload.get("type") != token_type:
            raise ValueError("Token类型错误")
        return payload
    except jwt.ExpiredSignatureError:
        raise ValueError("Token已过期")
    except jwt.JWTError:
        raise ValueError("Token无效")

二、登录接口

# api/auth.py
from fastapi import APIRouter, Depends, HTTPException
from passlib.context import CryptContext

router = APIRouter(prefix="/api/auth", tags=["auth"])
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

@router.post("/login")
async def login(username: str, password: str, db: AsyncSession = Depends(get_db)):
    # 验证用户
    user = await get_user_by_username(db, username)
    if not user or not pwd_context.verify(password, user.password_hash):
        raise HTTPException(status_code=401, detail="用户名或密码错误")
    
    # 生成Token
    access_token = create_access_token(user.id, user.username)
    refresh_token = create_refresh_token(user.id)
    
    return {
        "access_token": access_token,
        "refresh_token": refresh_token,
        "token_type": "bearer",
        "user": {
            "id": user.id,
            "username": user.username,
            "email": user.email
        }
    }

三、Token刷新

@router.post("/refresh")
async def refresh_token(refresh_token: str, db: AsyncSession = Depends(get_db)):
    # 验证Refresh Token
    try:
        payload = verify_token(refresh_token, token_type="refresh")
    except ValueError as e:
        raise HTTPException(status_code=401, detail=str(e))
    
    # 获取用户
    user = await get_user_by_id(db, payload["user_id"])
    if not user:
        raise HTTPException(status_code=401, detail="用户不存在")
    
    # 生成新的Access Token
    new_access_token = create_access_token(user.id, user.username)
    
    return {
        "access_token": new_access_token,
        "token_type": "bearer"
    }

四、认证依赖

# dependencies/auth.py
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: AsyncSession = Depends(get_db)
) -> User:
    token = credentials.credentials
    
    try:
        payload = verify_token(token, token_type="access")
    except ValueError as e:
        raise HTTPException(status_code=401, detail=str(e))
    
    user = await get_user_by_id(db, payload["user_id"])
    if not user:
        raise HTTPException(status_code=401, detail="用户不存在")
    
    return user

五、保护路由

@router.get("/me")
async def get_me(current_user: User = Depends(get_current_user)):
    return {
        "id": current_user.id,
        "username": current_user.username,
        "email": current_user.email
    }

@router.post("/conversations")
async def create_conversation(
    title: str,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    conversation = await create_conversation_for_user(db, current_user.id, title)
    return conversation

下一篇预告: 《Alembic数据库迁移:团队协作的最佳实践》