Day7-简单 JWT 认证

#王者杯·14天创作挑战营·第5期#

昨天搞定了依赖注入的基础,今天来解决认证问题。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鼓励!

dnSpy是目前业界广泛使用的一款.NET程序的反编译工具,支持32位和64位系统环境。它允许用户查看和编辑.NET汇编和反编译代码,以及调试.NET程序。该工具通常用于程序开发者在维护和调试过程中分析程序代码,尤其在源代码丢失或者无法获取的情况下,dnSpy能提供很大的帮助。 V6.1.8版本的dnSpy是在此系列软件更新迭代中的一个具体版本号,代表着该软件所具备的功能与性能已经达到了一个相对稳定的水平,对于处理.NET程序具有较高的可用性和稳定性。两个版本,即32位的dnSpy-net-win32和64位的dnSpy-net-win64,确保了不同操作系统架构的用户都能使用dnSpy进行软件分析。 32位的系统架构相较于64位,由于其地址空间的限制,只能支持最多4GB的内存空间使用,这在处理大型项目时可能会出现不足。而64位的系统能够支持更大的内存空间,使得在处理大型项目时更为方便。随着计算机硬件的发展,64位系统已经成为了主流,因此64位的dnSpy也更加受开发者欢迎。 压缩包文件名“dnSpy-net-win64.7z”和“dnSpy-net-win32.7z”中的“.7z”表示该压缩包采用了7-Zip压缩格式,它是一种开源的文件压缩软件,以其高压缩比著称。在实际使用dnSpy时,用户需要下载对应架构的压缩包进行解压安装,以确保软件能够正确运行在用户的操作系统上。 dnSpy工具V6.1.8版本的发布,对于.NET程序员而言,无论是32位系统还是64位系统用户,都是一个提升工作效率的好工具。用户可以根据自己计算机的操作系统架构,选择合适的版本进行下载使用。而对于希望进行深度分析.NET程序的开发者来说,这个工具更是不可或缺的利器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值