创新设计记录(二)

前言

​ 原来以为token只是简单的生成,验证即可。在后续的代码编写中,发现原有的写法无法满足需求,同时代码规范性也存在问题。查阅了fastapi官方文档,发现给出了JWT的标准写法,故参考官方文档,重新实现JWT令牌验证。
​# fastapi—JWT令牌验证

Token模型

  • 功能: Token 模型主要用于封装通过认证过程生成的访问令牌信息。它包含两个字段:
    • access_token: 一个字符串类型字段,用于存储实际的JWT(JSON Web Token)或其他形式的访问令牌。
    • token_type: 一个字符串类型字段,通常用于指明令牌的类型,比如 "bearer",这是OAuth 2.0中最常见的令牌类型。
  • 用途: 当用户成功认证后,服务器会生成一个新的访问令牌,并将其类型一并封装在 Token 对象中,然后返回给客户端。客户端之后在需要授权的API请求中,需提供这个access_token

TokenData模型

  • 功能: TokenData 模型代表了从访问令牌中解析出来的有效载荷(payload)数据,特别是在验证JWT时。它仅包含一个字段:
    • username: 一个可选的字符串类型字段(Union[str, None]),用于存储令牌中携带的用户名信息。设置为可选意味着在某些情况下,这个字段可能不存在于令牌中。
  • 用途: 当服务器接收到带有访问令牌的请求时,会验证并解码该令牌,从中提取出有效载荷数据(比如用户名),并使用 TokenData 模型来结构化这些数据,便于进一步处理,比如验证用户身份或权限。
from pydantic import BaseModel
class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    email: str| None

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

  • CryptContext: 这是passlib库中的一个类,用于简化密码哈希和验证的过程。它允许你无需直接处理不同的哈希算法细节,就能灵活地选择和切换密码加密方案。
  • schemes=[“bcrypt”]: 这里指定了使用的密码哈希方案为"bcrypt"。Bcrypt是一种安全的哈希函数,特别适合于存储密码,因为它设计了“慢哈希”机制来抵御暴力破解攻击。
  • deprecated=“auto”: 此参数让CryptContext自动处理过时的哈希算法。如果将来某个算法不再安全或被废弃,此设置可以帮助平滑迁移,确保新密码使用最新的安全算法,同时仍然能够验证使用旧算法哈希的密码。

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

  • OAuth2PasswordBearer: 这是FastAPI提供的一个类,用于实现OAuth 2.0的资源所有者密码凭据授予类型的流。这种授权类型允许用户通过提供他们的用户名和密码直接获取访问令牌(access token),常用于传统的Web应用登录流程。
  • tokenUrl=“token”: 指定了客户端获取访问令牌的端点URL。在这个例子中,客户端需要向/token端点发起POST请求,并提供正确的用户名和密码,以换取一个访问令牌。这个令牌随后会被客户端用于其他受保护资源的请求头中,作为身份验证手段。
from datetime import datetime, timedelta, timezone
from typing import Union

import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jwt.exceptions import InvalidTokenError
from sqlalchemy.orm import Session
from component.DB_engine import engine
from db.create_db import User
from config.server_config import JWT_ARGS

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# 生成token
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, JWT_ARGS["secret_key"], algorithm=JWT_ARGS["algorithm"])
    return encoded_jwt

async def get_current_user(token: str = Depends(oauth2_scheme)):
    print(token)
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token,JWT_ARGS["secret_key"], algorithms=[JWT_ARGS["algorithm"]])
        email: str = payload.get("sub")
        if email is None:
            raise credentials_exception
        #token_data = TokenData(email=email)
    except InvalidTokenError:
        raise credentials_exception
    with Session(engine) as session:
        user = session.query(User).filter(User.email == email).first()
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if not current_user.is_active:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


  • create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): 这个函数的作用是生成访问令牌(access token)。它接受两个参数:data 是一个字典,包含要编码到JWT中的信息;expires_delta 是一个可选的timedelta对象,用于指定令牌的过期时间。如果未提供expires_delta,则默认令牌15分钟后过期。

    函数首先复制data字典,并根据是否提供了过期时间来设置令牌的过期时间。然后使用jwt.encode方法对数据进行编码,生成JWT。编码时使用的密钥(JWT_ARGS["secret_key"])和算法(JWT_ARGS["algorithm"])是从配置文件中获取的。

  • get_current_user(token: str = Depends(oauth2_scheme)): 这个异步函数用于从请求中获取并验证当前用户的访问令牌。它依赖于oauth2_scheme,这是一个用于OAuth2密码承载令牌的FastAPI安全方案。

    函数首先打印出传入的令牌。然后尝试使用jwt.decode方法解码令牌,如果解码失败(例如,令牌无效或过期),则抛出一个HTTPException异常,状态码为401(未授权)。

    如果令牌解码成功,函数会从解码后的负载中获取用户的电子邮件地址,并在数据库中查找对应的用户。如果用户不存在,同样抛出401异常。如果用户存在,函数返回该用户对象。

  • get_current_active_user(current_user: User = Depends(get_current_user)): 这个异步函数是对get_current_user函数的进一步封装,用于确保当前用户是活跃的(即用户的is_active属性为True)。如果用户不是活跃状态,函数会抛出一个状态码为400的HTTPException异常,提示用户是“Inactive user”(非活跃用户)。

    这个函数通过依赖注入使用get_current_user函数的结果,即它期望current_user参数已经是一个有效的用户对象。

​ 总的来说,这三个函数共同工作,实现了用户认证流程中的几个关键步骤:生成访问令牌、验证令牌并获取当前用户信息、确保用户是活跃的。这在构建需要用户登录和权限管理的Web应用程序时非常有用。

如何产生token?

在登陆接口,若密码验证通过之后,会调用create_access_token函数,根据用户的email生成用户的token

async def login_user(login_form: LoginForm):
    """
       用户登录接口
       LoginForm 包含 email 和 password 字段
   """
    session = Session(bind=engine)
    try:
        # 根据邮箱查询用户
        user = session.query(User).filter(User.email == login_form.email).first()
        if not user:
            raise HTTPException(status_code=404, detail="用户不存在")

        # 对用户输入的密码进行MD5加密
        input_password_hash = md5(login_form.password.encode('utf-8')).hexdigest()

        # 验证密码是否正确
        if user.password != input_password_hash:
            raise HTTPException(status_code=401, detail="密码错误")

        # 登录成功,在此处生成token
        # token = generate_token(user.email)
        access_token_expires = timedelta(minutes=JWT_ARGS["expire_time"])
        access_token = create_access_token(
            data={"sub": user.email}, expires_delta=access_token_expires
        )
        return {"code": 200, "message": "登录成功", "data": {"token": access_token, "token_type": "bearer"}}

    except Exception as e:
        raise HTTPException(status_code=500, detail="登录失败,请重试")
    finally:
        session.close()

如何在接口使用token?

在接口函数参数中进行depend依赖注入即可;用户在访问该接口时首先判断携带的token是否有效。

async def update_info(info_form: InfoForm, current_user: User = Depends(get_current_active_user)):
    """
       用户更新个人信息接口
       info_form 包含 name ,age,sex,description 字段
       """
    session = Session(bind=engine)
    try:
        # 根据用户ID查询用户
        user = session.query(User).filter(User.email == info_form.email).first()
        if not user:
            raise HTTPException(status_code=404, detail="用户不存在")

        # 更新用户信息
        user.name = info_form.name
        user.age = info_form.age
        user.sex = info_form.sex
        user.description = info_form.description

        session.commit()
        return {"code": 200, "message": "个人信息更新成功"}

    except Exception as e:
        session.rollback()  # 发生异常时回滚事务
        raise HTTPException(status_code=500, detail="个人信息更新失败,请重试")
    finally:
        session.close()

运行实例

登陆成功,生成token返回前端。
在这里插入图片描述
携带正确token访问接口,访问成功(以更新用户信息接口作为示例)。
在这里插入图片描述
携带错误token访问接口,访问被拦截。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值