基于FastAPI用JWT实践用户登注册登陆

375 阅读5分钟

一、什么是 JWT 以及如何实现

JWT(JSON Web Token) 是一种基于 JSON 的访问令牌,用于在客户端和服务器之间传递身份验证数据。JWT 由三部分组成:头部(Header)负载(Payload)签名(Signature) 。它的结构如下:

header.payload.signature

JWT 工作流程

  1. 客户端在登录时向服务器发送凭证(如用户名和密码)。
  2. 服务器验证凭证后,生成一个包含用户信息的 JWT,并将其返回给客户端。
  3. 客户端将 JWT 存储在浏览器的 LocalStorageCookie 中,以便在后续请求中附带该令牌进行身份验证。
  4. 服务器在接收到请求时,会解码和验证 JWT,以确认请求是否合法。

二、JWT 的实现:代码示例(包括详细注释)

以下代码展示了在 FastAPI 中如何实现 JWT 验证机制,包括注册、登录和访问受保护的路由。

1. 安装依赖

pip install fastapi uvicorn pyjwt passlib[bcrypt]

2. 生成 JWT 令牌和解码验证

代码文件:app/core/auth.py

import jwt  # 导入用于编码和解码 JWT 的库
from datetime import datetime, timedelta
from app.config import SECRET_KEY  # 从配置文件中导入密钥

# 定义生成访问令牌的函数
def create_access_token(data: dict, expires_delta: timedelta = timedelta(minutes=30)):
    """
    创建 JWT 访问令牌,包含用户信息(`data`)和过期时间。
    
    参数:
    - data: dict,包含要编码的用户信息
    - expires_delta: timedelta,令牌有效期,默认为30分钟
    
    返回:
    - str: 加密后的 JWT 令牌
    """
    to_encode = data.copy()  # 复制数据字典,以便修改而不影响原数据
    expire = datetime.utcnow() + expires_delta  # 设置过期时间
    to_encode.update({"exp": expire})  # 将过期时间添加到数据中
    return jwt.encode(to_encode, SECRET_KEY, algorithm="HS256")  # 使用 HS256 算法生成并返回 JWT

# 定义解码 JWT 令牌的函数
def decode_access_token(token: str):
    """
    验证并解码 JWT 令牌。
    
    参数:
    - token: str,待解码的 JWT 令牌
    
    返回:
    - dict: 解码后的数据
    - None: 如果令牌无效或已过期
    """
    try:
        # 使用 SECRET_KEY 解码 JWT,并返回载荷数据
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        return payload if "exp" in payload else None  # 确保 payload 包含过期时间
    except jwt.ExpiredSignatureError:
        return None  # 返回 None 表示令牌已过期
    except jwt.InvalidTokenError:
        return None  # 返回 None 表示令牌无效

3. 注册、登录和受保护的路由

代码文件:app/routers/user.py

from fastapi import APIRouter, HTTPException, Depends
from passlib.hash import bcrypt

from app.core.auth import create_access_token  # 导入用于创建 JWT 的函数
from app.core.dependencies import get_current_user  # 导入验证用户身份的依赖项
from app.models.user import User  # 用户模型,用于数据库操作
from app.schemas.user import UserCreate, Token, UserLogin  # 导入请求和响应的 Pydantic 模型

router = APIRouter()

# 注册新用户的路由
@router.post("/register", response_model=Token)
async def register(user: UserCreate):
    """
    用户注册端点,创建新用户并生成访问令牌。
    """
    # 检查用户名是否已存在
    user_obj = await User.filter(username=user.username).first()
    if user_obj:
        raise HTTPException(status_code=400, detail="Username already exists")

    # 对用户密码进行哈希加密存储
    hashed_password = bcrypt.hash(user.password)
    user_obj = await User.create(username=user.username, password_hash=hashed_password)

    # 创建访问令牌并返回给客户端
    access_token = create_access_token(data={"sub": user_obj.username})
    return Token(access_token=access_token, token_type="bearer")

# 用户登录的路由
@router.post("/login", response_model=Token)
async def login(user: UserLogin):
    """
    用户登录端点,验证用户名和密码并生成访问令牌。
    """
    user_obj = await User.filter(username=user.username).first()
    if not user_obj or not user_obj.verify_password(user.password):
        raise HTTPException(status_code=400, detail="Incorrect username or password")

    # 生成 JWT 并返回给客户端
    access_token = create_access_token(data={"sub": user_obj.username})
    return Token(access_token=access_token, token_type="bearer")

# 受保护的路由,只有经过身份验证的用户才能访问
@router.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
    """
    获取当前用户信息,验证 JWT。
    """
    return {"username": current_user.username}

三、JWT 是否安全

JWT 的安全性取决于如何实现和使用它。JWT 是一种无状态的身份验证方式,具备灵活性和高效性,但有潜在的安全风险。以下是确保 JWT 安全的建议:

优点:

  1. 无状态:JWT 是无状态的,不需要在服务器端存储会话数据,适合分布式系统。
  2. 灵活性:JWT 是自包含的,前端可以携带该令牌实现身份验证。
  3. 标准化的签名算法:JWT 使用加密签名来保证数据完整性,防止被篡改。

缺点:

  1. 令牌泄露风险:JWT 一旦泄露,攻击者可以在有效期内完全访问资源。
  2. 难以撤销:JWT 通常无状态,不易提前失效(除非使用黑名单等方案)。
  3. 体积较大:JWT 的体积相对较大,增加了网络传输成本。

四、JWT 安全性建议

  1. 使用 HTTPS

    • 所有与 JWT 相关的请求都应在 HTTPS 下进行,以防止中间人攻击截获令牌。
  2. 设置合理的过期时间

    • 建议设置较短的访问令牌有效期,例如 15 到 30 分钟,结合长效刷新令牌(Refresh Token)来维持会话。
  3. 使用安全的签名算法

    • 使用可靠的算法(如 HS256 或 RS256)签名令牌,避免使用 none 算法。
  4. 保护密钥(SECRET_KEY)

    • SECRET_KEY 必须足够复杂并且仅在服务器端存储,不应暴露在客户端或代码中。
  5. 敏感信息尽量不放在 JWT 中

    • 避免在 JWT 中存储敏感信息(如密码、敏感的个人数据),仅保存基本身份信息(如用户 ID 或角色)。
  6. 使用刷新令牌(Refresh Token)

    • 通过长效的刷新令牌(Refresh Token)来刷新短效访问令牌,以保持会话的安全性。刷新令牌应存储在受保护的地方(例如 HttpOnly Cookie)。
  7. 存储在 HttpOnly 和 Secure Cookie 中

    • 可以选择将 JWT 存储在 HttpOnly 和 Secure Cookie 中,以减少 XSS 风险,使 JWT 不可被 JavaScript 访问。
  8. 防范重放攻击

    • 实现重放保护机制,例如在服务器端记录 JWT 的签发时间或唯一 ID (jti),并监测异常请求。
  9. 实现令牌撤销

    • 使用黑名单机制可以撤销部分令牌,在用户注销或异常活动时立即失效某些 JWT。

五、总结

使用 JWT 进行身份认证需要特别注意安全性,尤其是:

  • 使用 HTTPS、设置合理的过期时间和刷新机制。
  • 避免在令牌中包含敏感信息。
  • 结合刷新令牌和令牌黑名单机制来管理令牌的有效性。

通过这些最佳实践,JWT 认证机制可以相对安全地用于现代 Web 应用中。