Skip to content

认证与权限

前面几篇的接口任何人都能调——没有"你是谁"的概念。真实项目需要认证(知道请求是谁发的)和权限(这个用户能不能做这件事)。FastAPI 生态里最常见的方案是 JWT Token。

一、认证的基本流程

密码不能明文存数据库,Token 不用每次传密码——这两件事决定了认证的基本流程:

两步:登录时拿 Token,后续请求带着 Token。前端 06 篇封装的 request 函数已经做了"从 localStorage 取 Token、放进 Authorization 头"——后端要做的就是验证这个 Token。

二、密码哈希

用户密码绝对不能明文存数据库——数据库泄露等于所有密码泄露。存的是密码的哈希值(单向不可逆):

bash
uv add passlib[bcrypt]
python
from passlib.context import CryptContext

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


def hash_password(password: str) -> str:
    return pwd_context.hash(password)


def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)


# 注册时存哈希
stored_hash = hash_password("mypassword123")

# 登录时验证
print(verify_password("mypassword123", stored_hash))   # True
print(verify_password("wrongpassword", stored_hash))    # False

hash_password 把明文密码转成一段完全看不出原文的字符串(哈希值),verify_password 把用户输入的明文跟存储的哈希比对。bcrypt 算法自带盐值,同一个密码每次 hash 出来的结果都不一样,但 verify 能正确比对——这是它比简单 MD5/SHA 强的地方。

三、JWT Token

JWT(JSON Web Token)是一段编码过的字符串,里面包含用户信息和签名。后端签发 Token,前端带着它发请求,后端验证签名通过后从 Token 里取出用户信息。

bash
uv add python-jose[cryptography]
python
from jose import jwt, JWTError
from datetime import datetime, timedelta, timezone

SECRET_KEY = "your-secret-key-change-in-production"  # 生产环境必须换一个足够随机的
ALGORITHM = "HS256"
TOKEN_EXPIRE_HOURS = 24


def create_token(data: dict) -> str:
    """生成 JWT Token。data 里放要编码的用户信息。"""
    payload = data.copy()
    expire = datetime.now(timezone.utc) + timedelta(hours=TOKEN_EXPIRE_HOURS)
    payload.update({"exp": expire})  # 过期时间
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)


def decode_token(token: str) -> dict:
    """解析 Token,返回里面的数据。签名不对或过期会抛异常。"""
    return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])


# 签发
token = create_token({"sub": "user_001", "name": "张三"})
print(token)  # eyJhbGciOi...

# 解析
payload = decode_token(token)
print(payload)  # {'sub': 'user_001', 'name': '张三', 'exp': ...}

create_token 把用户信息(用户 ID、用户名)编码进 Token,decode_token 把它解出来。Token 里不要放敏感信息——JWT 的编码只是 base64,不是加密,任何人都能解码看到内容。签名只保证"这个 Token 没被篡改",不保证"内容是加密的"。

SECRET_KEY 是签名的密钥——只有知道这个密钥才能签发合法 Token。生产环境必须用足够随机的长字符串openssl rand -hex 32 生成一个),泄露了等于谁都能伪造 Token。

四、登录接口

把密码哈希和 JWT 串起来:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# 模拟用户数据库(后面换成真实数据库)
users_db = {
    "zhangsan": {
        "id": 1,
        "username": "zhangsan",
        "hashed_password": hash_password("password123"),
    }
}


class LoginRequest(BaseModel):
    username: str
    password: str


@app.post("/login")
def login(req: LoginRequest):
    user = users_db.get(req.username)
    if not user or not verify_password(req.password, user["hashed_password"]):
        raise HTTPException(status_code=401, detail="用户名或密码错误")

    token = create_token({"sub": str(user["id"]), "username": user["username"]})
    return {"access_token": token, "token_type": "bearer"}

前端拿到 access_token 后存进 localStorage,后续请求放在 Authorization: Bearer <token> 头里——跟前端 06 篇的封装完全对应。

HTTPException(status_code=401) 是 FastAPI 抛 HTTP 错误的方式——返回 401 状态码和错误详情。用户名不存在或密码不对,都返回同样的 401,不告诉具体是用户名错还是密码错,防止攻击者用错误信息猜测用户名。

五、保护接口——验证 Token

需要一个机制:请求带了合法 Token 才能访问,否则返回 401。FastAPI 用依赖注入实现:

python
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")


async def get_current_user(token: str = Depends(oauth2_scheme)):
    """从 Token 解析当前用户。Token 无效时抛 401。"""
    try:
        payload = decode_token(token)
        user_id = payload.get("sub")
        if user_id is None:
            raise HTTPException(status_code=401, detail="无效的 Token")
    except JWTError:
        raise HTTPException(status_code=401, detail="无效的 Token")

    # 根据 user_id 查用户(这里简化,直接返回 payload)
    return {"id": int(user_id), "username": payload.get("username")}

在需要登录的接口上加 Depends(get_current_user)

python
@app.get("/me")
def get_profile(current_user: dict = Depends(get_current_user)):
    return current_user


@app.get("/articles")
def list_articles(current_user: dict = Depends(get_current_user)):
    return [{"id": 1, "title": "Python 入门"}]

OAuth2PasswordBearer(tokenUrl="login") 自动从请求头 Authorization: Bearer <token> 里提取 Token 字符串,传给 get_current_user没带 Token 或 Token 无效,get_current_user 抛 401,接口不会执行

/me/articles 加了 Depends(get_current_user),变成需要登录才能访问的接口。不加这个依赖的接口(比如 /login)仍然可以匿名访问——FastAPI 的权限控制是逐接口声明的,不是全局一刀切。