FastAPI 学习之路(二十八)使用密码和 Bearer 的简单 OAuth2

OAuth2 规定在使用(我们打算用的)「password 流程」时,客户端/用户必须将 username 和 password 字段作为表单数据发送。我们看下在我们应该去如何实现呢。

我们写一个登录接口,默认返回token和token_type

from fastapi import FastAPI, Depends, status, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

from pydantic import BaseModel
from typing import Optional

oauth2_scheme =  OAuth2PasswordBearer(tokenUrl="token")

fake_db_users ={
    "mrli": {
        "username": "mrli",
        "full_name": "mrli_hanjing",
        "email": "mrli@qq.com",
        "hashed_password": "mrli",
        "disabled": False
    }
}

app = FastAPI()

def fake_hash_password(password: str):
    """模拟将密码加密"""
    return password


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


class UserInDB(User):
    hashed_password: str

def get_user(db_users, username: str):
    if username in db_users:
        user_dict = db_users[username]
        return UserInDB(**user_dict)

def fake_decode_token(token):
    """我们模拟返回的token值就是username,所以下面可以直接传token"""
    user = get_user(fake_db_users, token)
    return user

def get_current_user(token: str = Depends(oauth2_scheme)):
    user = fake_decode_token(token)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication",
            headers={"WWW-Autehticated": "Bearer"}
        )
    return user


@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    校验密码
    目前我们已经从数据库中获取了用户数据,但尚未校验密码。
    让我们首先将这些数据放入 Pydantic UserInDB 模型中。
    永远不要保存明文密码,因此,我们将使用(伪)哈希密码系统。
    如果密码不匹配,我们将返回同一个错误。
    """
    user_dict = fake_db_users.get(form_data.username)
    if not user_dict:
        raise HTTPException(status_code=400, detail="Invalid username or password")
    user = UserInDB(**user_dict)
    hashed_password = fake_hash_password(form_data.password)
    if hashed_password != user.hashed_password:
        raise HTTPException(status_code=400, detail="Invalid username or password")
    return {"access_token": user.username, "token_type": "bearer"}

@app.get("/users/me")
def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user


if __name__ == '__main__':
    import uvicorn
    uvicorn.run("main:app", reload=True, debug=True)

 我们测试下登录接口

接下来再测试下带认证的/users/me接口 (我们发现不能通过)

 接下来我们带上认证(校验通过,返回了我们想要的数据) 

在我们的代码中,有这样一句:

UserInDB(**user_dict)

 作用是直接将user_dict的键和值作为关键字参数传递,也就是python的解包,等同于:

UserInDB(
    username = user_dict["username"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    disabled = user_dict["disabled"],
    hashed_password = user_dict["hashed_password"],
)

假如我们把user的状态disabled改为True 

fake_db_users ={
    "mrli": {
        "username": "mrli",
        "full_name": "mrli_hanjing",
        "email": "mrli@qq.com",
        "hashed_password": "mrli",
        "disabled": False
    }
}

我们不想让disabled为True的用户获取信息,那么我们如何实现呢?

from fastapi import FastAPI, Depends, status, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

from pydantic import BaseModel
from typing import Optional

oauth2_scheme =  OAuth2PasswordBearer(tokenUrl="token")

fake_db_users ={
    "mrli": {
        "username": "mrli",
        "full_name": "mrli_hanjing",
        "email": "mrli@qq.com",
        "hashed_password": "mrli",
        "disabled": True
    }
}

app = FastAPI()

def fake_hash_password(password: str):
    """模拟将密码加密"""
    return password


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


class UserInDB(User):
    hashed_password: str

def get_user(db_users, username: str):
    if username in db_users:
        user_dict = db_users[username]
        return UserInDB(**user_dict)

def fake_decode_token(token):
    """我们模拟返回的token值就是username,所以下面可以直接传token"""
    user = get_user(fake_db_users, token)
    return user

def get_current_user(token: str = Depends(oauth2_scheme)):
    user = fake_decode_token(token)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication",
            headers={"WWW-Autehticated": "Bearer"}
        )
    return user

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


@app.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    校验密码
    目前我们已经从数据库中获取了用户数据,但尚未校验密码。
    让我们首先将这些数据放入 Pydantic UserInDB 模型中。
    永远不要保存明文密码,因此,我们将使用(伪)哈希密码系统。
    如果密码不匹配,我们将返回同一个错误。
    """
    user_dict = fake_db_users.get(form_data.username)
    if not user_dict:
        raise HTTPException(status_code=400, detail="Invalid username or password")
    user = UserInDB(**user_dict)
    hashed_password = fake_hash_password(form_data.password)
    if hashed_password != user.hashed_password:
        raise HTTPException(status_code=400, detail="Invalid username or password")
    return {"access_token": user.username, "token_type": "bearer"}

@app.get("/users/me")
def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user


if __name__ == '__main__':
    import uvicorn
    uvicorn.run("main:app", reload=True, debug=True)

其实很简单,我们就是在获取用户信息依赖的基础上增加了另一个是否是active的判断的依赖。

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

@app.get("/users/me")
def read_users_me(current_user: User = Depends(get_current_active_user)):
   
    return current_user

FastAPI 中实现 OAuth2 认证是构建安全 API 的重要步骤。FastAPI 提供了对 OAuth2 的内置支持,特别是通过 `OAuth2PasswordBearer` 类来简化基于密码OAuth2 流程的实现。以下是实现 OAuth2 认证的关键步骤示例代码。 ### 1. 使用 `OAuth2PasswordBearer` 设置安全方案 FastAPI 通过 `OAuth2PasswordBearer` 类来声明 OAuth2 密码流模式的安全方案。该类会自动将认证机制添加到 OpenAPI 文档中,并确保在接口请求时要求提供有效的访问令牌 [^2]。 ```python from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from pydantic import BaseModel from typing import Optional app = FastAPI() # 定义 OAuth2PasswordBearer 实例 oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") ``` ### 2. 定义用户模型令牌响应模型 为了处理用户信息生成令牌,需要定义用户模型令牌响应模型。 ```python class User(BaseModel): username: str email: Optional[str] = None full_name: Optional[str] = None disabled: Optional[bool] = None class UserInDB(User): hashed_password: str class Token(BaseModel): access_token: str token_type: str ``` ### 3. 模拟用户数据库密码哈希 在实际应用中,用户信息密码哈希应从数据库中获取。以下是一个简单的模拟实现。 ```python from passlib.context import CryptContext pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # 模拟用户数据库 fake_users_db = { "johndoe": { "username": "johndoe", "full_name": "John Doe", "email": "johndoe@example.com", "hashed_password": pwd_context.hash("secret"), "disabled": False, } } def get_user(db, username: str): if username in db: return UserInDB(**db[username]) return None def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) ``` ### 4. 实现 `/token` 端点 该端点用于接收用户名密码,并返回访问令牌。 ```python from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from datetime import datetime, timedelta # JWT 配置 SECRET_KEY = "your-secret-key" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @app.post("/token", response_model=Token) async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): user = get_user(fake_users_db, form_data.username) if not user or not verify_password(form_data.password, user.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} ``` ### 5. 受保护的端点 使用 `Depends(oauth2_scheme)` 可以保护端点,只有提供有效令牌的请求才能访问。 ```python @app.get("/users/me", response_model=User) async def read_users_me(token: str = Depends(oauth2_scheme)): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) except JWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) user = get_user(fake_users_db, username) if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found", headers={"WWW-Authenticate": "Bearer"}, ) return user ``` ### 6. 安全建议 为了确保 API 的安全性,应遵循以下最佳实践 [^3]: - 使用 HTTPS 加密所有通信。 - 定期更新依赖库框架。 - 实施速率限制以防止滥用。 - 进行安全审计渗透测试。 - 避免在错误响应中泄露敏感信息。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值