Day7-JWT 认证

王者杯·14天创作挑战营·第5期 10w+人浏览 621人参与

昨天搞定了依赖注入的基础,今天来解决认证问题。Day6我们留下了了一个问题:用全局变量current_user_id来记录登录状态,这在多进程环境下会有问题。今天用JWT来彻底解决这个问题。

现在的问题

Day6我们用全局变量current_user_id来记录登录状态,还有很多重复的认证和权限检查代码。全局变量在多进程环境下会有问题,而且每个API都要重复写相同的检查逻辑。

解决思路

用JWT(JSON Web Token)替换全局变量。JWT的好处:

  • 无状态:服务器不需要存储用户状态,所有信息都在token里
  • 可验证:用密钥签名,防止篡改
  • 跨服务:多个服务可以用同一个密钥验证token

然后用依赖注入把认证和权限检查抽出来,就像Day6的分页依赖一样。

我们分几步来改:

  1. 安装JWT库并配置密钥
  2. 创建JWT工具函数
  3. 实现认证与权限依赖
  4. 更新数据模型
  5. 更新登录API
  6. 更新文章API使用认证和权限依赖
  7. 测试JWT认证

第一步:安装JWT库

pip install python-jose[cryptography]

第二步:JWT配置和工具函数

创建auth.py

# v7_jwt/auth.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from fastapi import HTTPException, status

# JWT配置
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    """创建JWT token"""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def verify_token(token: str) -> dict:
    """验证JWT token并返回payload"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id_str: str = payload.get("sub")
        if user_id_str is None:
            raise HTTPException(
                status_code=401,
                detail="无效的认证凭据",
                headers={"WWW-Authenticate": "Bearer"},
            )
        # 转换为整数并验证
        try:
            user_id = int(user_id_str)
        except (ValueError, TypeError):
            raise HTTPException(
                status_code=401,
                detail="无效的用户ID",
                headers={"WWW-Authenticate": "Bearer"},
            )
        return {"user_id": user_id}
    except JWTError:
        raise HTTPException(
            status_code=401,
            detail="无效的认证凭据",
            headers={"WWW-Authenticate": "Bearer"},
        )

第三步:认证与权限依赖

dependencies.py中添加认证和权限相关的依赖:

# v7_jwt/dependencies.py
from fastapi import Depends, HTTPException, status, Query
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from auth import verify_token
from database import AsyncSessionLocal
import crud

# 原有的依赖...
async def get_async_db():
    """数据库会话依赖"""
    # ... 保持不变

def get_pagination(
    page: int = Query(1, ge=1, description="页码,从1开始"),
    size: int = Query(10, ge=1, le=100, description="每页数量,最大100")
):
    """分页参数依赖"""
    # ... 保持不变

# 新增:认证和权限依赖
security = HTTPBearer()

# 认证依赖:解决"你是谁"的问题
async def get_current_user_id(
    credentials: HTTPAuthorizationCredentials = Depends(security)
) -> int:
    """获取当前用户ID(认证依赖)"""
    token = credentials.credentials
    payload = verify_token(token)
    return payload["user_id"]

async def get_current_user(
    user_id: int = Depends(get_current_user_id),
    db: AsyncSession = Depends(get_async_db)
):
    """获取当前用户对象(认证依赖)"""
    user = await crud.get_user_by_id(db, user_id)
    if not user:
        raise HTTPException(
            status_code=401,
            detail="用户不存在"
        )
    return user

# 权限依赖:解决"你能做什么"的问题

async def verify_post_owner(
    post_id: int,
    current_user_id: int = Depends(get_current_user_id),  # 先认证:从JWT获取用户ID
    db: AsyncSession = Depends(get_async_db)
):
    """验证文章所有权(权限依赖)
    
    自动检查:用户身份认证 → 文章存在性 → 所有权验证
    成功时返回文章对象,失败时抛出HTTP异常(401/403/404)
    """
    post = await crud.get_post_by_id(db, post_id)
    if not post:
        raise HTTPException(status_code=404, detail="文章不存在")
    
    if post.author_id != current_user_id:
        raise HTTPException(status_code=403, detail="只能操作自己的文章")
    
    return post

第四步:更新数据模型

首先在 schemas.py 中添加认证相关的模型:

# v7_jwt/schemas.py
from pydantic import BaseModel
from datetime import datetime
from typing import Optional

# 原有的模型保持不变
class TokenResponse(BaseModel):
    access_token: str
    token_type: str
    expires_in: int

第五步:更新登录API

现在登录API要返回JWT token而不是设置全局变量:

# v7_jwt/main.py
from fastapi import FastAPI, HTTPException, Depends, status, Request,Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from datetime import timedelta

from dependencies import get_async_db, get_pagination, get_current_user, get_current_user_id, verify_post_owner
from schemas import UserRegister, UserResponse, UserLogin, PostCreate, PostResponse,TokenResponse
from auth import create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES
import crud
import models

app = FastAPI(
    title="博客系统API v7.0",
    description="7天FastAPI学习系列 - Day7JWT版本",
    version="7.0.0"
)

# 改造登录API - 返回JWT token而不是设置全局变量
@app.post("/users/login", response_model=TokenResponse)
async def login_user(login_data: UserLogin, db: AsyncSession = Depends(get_async_db)):
    """用户登录"""
    logger.info(f"用户登录请求: 账户={login_data.account}")
    
    user = await crud.authenticate_user(db, login_data.account, login_data.password)
    if not user:
        logger.warning(f"登录失败: 账号或密码错误 - 账号={login_data.account}")
        raise HTTPException(status_code=401, detail="用户名或密码错误")

    logger.info(f"用户登录成功: ID={user.id}, 用户名={user.username}")

    # Day7 新增:创建并返回JWT token
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": str(user.id)}, 
        expires_delta=access_token_expires
    )
    
    return {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
    }
    
# 改造用户资料API - 使用JWT认证依赖
@app.get("/users/profile", response_model=UserResponse)
async def get_current_user_profile(
    current_user = Depends(get_current_user)
):
    """获取当前登录用户信息(Day7 JWT认证版)"""
    # Day6及之前:从全局变量获取用户
    # Day7:通过JWT依赖自动获取认证用户
    return UserResponse(
        id=current_user.id,
        username=current_user.username,
        email=current_user.email,
        created_at=current_user.created_at
    )

第六步:更新文章API使用认证和权限依赖

现在所有需要登录的API都可以用依赖注入了:

# 创建文章 - 使用认证依赖
@app.post("/posts", response_model=PostResponse)
async def create_post(
    post: PostCreate,
    current_user_id: int = Depends(get_current_user_id),  # 依赖注入获取用户ID
    db: AsyncSession = Depends(get_async_db)
):
    """创建新文章"""
    # 不用再写 if not current_user_id 了!
    db_post = await crud.create_post(db, post, author_id=current_user_id)
    return PostResponse(
        id=db_post.id,
        title=db_post.title,
        content=db_post.content,
        author_id=db_post.author_id,
        created_at=db_post.created_at,
        updated_at=db_post.updated_at
    )

# 更新文章 - 使用权限依赖
@app.put("/posts/{post_id}", response_model=PostResponse)
async def update_post(
    post_id: int,  # 显式声明路径参数
    post_data: PostCreate, 
    post = Depends(verify_post_owner),  # 依赖注入验证权限并获取文章
    db: AsyncSession = Depends(get_async_db)
):
    """更新文章"""
    # 不用再写权限检查了!verify_post_owner已经帮我们做了
    updated_post = await crud.update_post(db, post, post_data)
    return PostResponse(
        id=updated_post.id,
        title=updated_post.title,
        content=updated_post.content,
        author_id=updated_post.author_id,
        created_at=updated_post.created_at,
        updated_at=updated_post.updated_at
    )

# 删除文章 - 使用权限依赖
@app.delete("/posts/{post_id}")
async def delete_post_api(    
    post_id: int, 
    post = Depends(verify_post_owner),
    db: AsyncSession = Depends(get_async_db)
    ):
    """删除文章""" 
    success = await crud.delete_post(db, post_id)
    if not success:
        raise HTTPException(status_code=500, detail="删除文章失败")
    
    return {"message": "文章删除成功"}

第七步:测试JWT认证

1. 登录获取token

curl -X POST "http://localhost:8000/users/login" \
  -H "Content-Type: application/json" \
  -d '{"account": "loji@qq.com","password": "Test136!"}'

# 返回:
# {
#   "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
#   "token_type": "bearer",
#   "expires_in": 1800
# }

2. 使用token访问需要认证的API

# 获取当前用户信息
curl "http://localhost:8000/users/profile" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# 创建文章
curl -X POST "http://localhost:8000/posts" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{"title": "海贼王1159话情报", "content": "洛克斯真名为“戴维•D•吉贝克”!夏琪是神之谷大赛的奖品"}'

3. 测试权限控制

# 尝试编辑别人的文章(应该返回403错误)
curl -X PUT "http://localhost:8000/posts/999" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{"title": "海贼王最终章", "content": "千两道化-巴基当上海贼王,世界迎来红鼻子时代!"}'

今日总结

  1. JWT工具函数 - 创建和验证token,替代全局变量
  2. 认证依赖 - get_current_user_idget_current_user解决"你是谁"
  3. 权限依赖 - verify_post_owner解决"你能做什么"

其他需要认证或权限控制的接口也可以采用类似的依赖注入方式实现。

系列回顾

通过这7天的学习,我们从零开始构建了一个完整的博客系统。虽然项目简单,但是我觉得对于想快速入门FastAPI的同学来说,应该还是有一定帮助的。

项目开源

代码已上传github上,这是github仓库地址,如果项目对你有帮助,欢迎点个Star鼓励!

### 集成JWT认证到Django-Ninja 在现代Web开发中,JSON Web Tokens (JWT) 是一种广泛使用的身份验证机制。对于 `django-ninja` 这一轻量级API框架来说,集成JWT可以显著增强其安全性并简化用户会话管理。 #### 使用PyJWT库实现JWT认证 为了在 `django-ninja` 中启用JWT支持,通常需要依赖于第三方库如 `PyJWT` 或者更高级别的封装工具如 `djangorestframework-simplejwt` 来处理令牌的生成和解析过程[^1]。 以下是具体实现方法: 1. **安装必要的包** 开始之前,请确保已安装所需的Python包: ```bash pip install djangorestframework_simplejwt pyjwt django-ninja ``` 2. **配置settings.py** 将以下内容添加至项目的 `settings.py` 文件中以便激活Simple JWT作为默认的身份验证类之一: ```python INSTALLED_APPS += ['rest_framework', 'rest_framework_simplejwt'] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ) } ``` 3. **创建自定义Auth方案** 创建一个新的文件夹结构用于存放自定义的身份验证逻辑, 并在此基础上扩展原有的功能来适配ninja的需求. ```python import jwt from datetime import timedelta, datetime from ninja.security import HttpBearer SECRET_KEY = "your_secret_key" class CustomAuth(HttpBearer): def authenticate(self, request, token): try: payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) return {"id":payload['user_id']} except Exception as e: raise ValueError(f"Invalid Token {e}") auth_scheme = CustomAuth() ``` 4. **保护路由资源** 当前已经具备了基本的能力去校验传入请求中的token有效性,在实际应用过程中可以通过装饰器的形式指定哪些接口需要经过授权才能访问。 ```python from ninja import Router router = Router(auth=auth_scheme) @router.get("/protected/") def protected(request): return f"Protected endpoint - User ID:{request.auth['id']}" ``` 以上步骤展示了如何利用现有的DRF组件快速搭建起一套基于JWT的工作流,并将其无缝嵌入到Ninja API当中[^2]。 ```python import jwt from datetime import timedelta, datetime from ninja.security import HttpBearer SECRET_KEY = "supersecret" class AuthBearer(HttpBearer): def authenticate(self, request, token): try: decoded_payload = jwt.decode( token, SECRET_KEY, algorithms=["HS256"] ) expiration_time = datetime.fromtimestamp(decoded_payload["exp"]) if expiration_time < datetime.utcnow(): return None return {'sub':decoded_payload['sub'], 'role':'admin'} except Exception as ex: print(ex) return None auth_bearer = AuthBearer() @router.post('/login') def login(request, username:str, password:str): # Assume we have some logic here validating credentials... issued_at = int(datetime.now().timestamp()) expires_in_seconds = 86400 # One day validity period. exp = issued_at + expires_in_seconds encoded_jwt = jwt.encode({ 'iss': 'myserver', 'aud': 'webapp', 'iat':issued_at , 'exp': exp, 'sub':username}, key=SECRET_KEY , algorithm='HS256') return {"access_token":encoded_jwt,"expires_in":expires_in_seconds} ``` ### 注意事项 尽管上述代码片段提供了一个简单的解决方案,但在生产环境中部署时还需要考虑更多因素比如刷新策略、黑名单维护以及跨域资源共享等问题[^3]. 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值