在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1 @@
"""认证模块JWT 令牌管理与 FastAPI 依赖注入。"""

View File

@@ -0,0 +1,67 @@
"""
FastAPI 依赖注入:从 JWT 提取当前用户信息。
用法:
@router.get("/protected")
async def protected_endpoint(user: CurrentUser = Depends(get_current_user)):
print(user.user_id, user.site_id)
"""
from dataclasses import dataclass
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError
from app.auth.jwt import decode_access_token
# Bearer token 提取器
_bearer_scheme = HTTPBearer(auto_error=True)
@dataclass(frozen=True)
class CurrentUser:
"""从 JWT 解析出的当前用户上下文。"""
user_id: int
site_id: int
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(_bearer_scheme),
) -> CurrentUser:
"""
FastAPI 依赖:从 Authorization header 提取 JWT验证后返回用户信息。
失败时抛出 401。
"""
token = credentials.credentials
try:
payload = decode_access_token(token)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的令牌",
headers={"WWW-Authenticate": "Bearer"},
)
user_id_raw = payload.get("sub")
site_id = payload.get("site_id")
if user_id_raw is None or site_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="令牌缺少必要字段",
headers={"WWW-Authenticate": "Bearer"},
)
try:
user_id = int(user_id_raw)
except (TypeError, ValueError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="令牌中 user_id 格式无效",
headers={"WWW-Authenticate": "Bearer"},
)
return CurrentUser(user_id=user_id, site_id=site_id)

View File

@@ -0,0 +1,112 @@
"""
JWT 令牌生成、验证与解码。
- access_token短期有效默认 30 分钟),用于 API 请求认证
- refresh_token长期有效默认 7 天),用于刷新 access_token
- payload 包含 user_id、site_id、令牌类型access / refresh
- 密码哈希直接使用 bcrypt 库passlib 与 bcrypt>=4.1 存在兼容性问题)
"""
from datetime import datetime, timedelta, timezone
import bcrypt
from jose import JWTError, jwt
from app import config
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""校验明文密码与哈希是否匹配。"""
return bcrypt.checkpw(
plain_password.encode("utf-8"), hashed_password.encode("utf-8")
)
def hash_password(password: str) -> str:
"""生成密码的 bcrypt 哈希。"""
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def create_access_token(user_id: int, site_id: int) -> str:
"""
生成 access_token。
payload: sub=user_id, site_id, type=access, exp
"""
expire = datetime.now(timezone.utc) + timedelta(
minutes=config.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
)
payload = {
"sub": str(user_id),
"site_id": site_id,
"type": "access",
"exp": expire,
}
return jwt.encode(payload, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM)
def create_refresh_token(user_id: int, site_id: int) -> str:
"""
生成 refresh_token。
payload: sub=user_id, site_id, type=refresh, exp
"""
expire = datetime.now(timezone.utc) + timedelta(
days=config.JWT_REFRESH_TOKEN_EXPIRE_DAYS
)
payload = {
"sub": str(user_id),
"site_id": site_id,
"type": "refresh",
"exp": expire,
}
return jwt.encode(payload, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM)
def create_token_pair(user_id: int, site_id: int) -> dict[str, str]:
"""生成 access_token + refresh_token 令牌对。"""
return {
"access_token": create_access_token(user_id, site_id),
"refresh_token": create_refresh_token(user_id, site_id),
"token_type": "bearer",
}
def decode_token(token: str) -> dict:
"""
解码并验证 JWT 令牌。
返回 payload dict包含 sub、site_id、type、exp。
令牌无效或过期时抛出 JWTError。
"""
try:
payload = jwt.decode(
token, config.JWT_SECRET_KEY, algorithms=[config.JWT_ALGORITHM]
)
return payload
except JWTError:
raise
def decode_access_token(token: str) -> dict:
"""
解码 access_token 并验证类型。
令牌类型不是 access 时抛出 JWTError。
"""
payload = decode_token(token)
if payload.get("type") != "access":
raise JWTError("令牌类型不是 access")
return payload
def decode_refresh_token(token: str) -> dict:
"""
解码 refresh_token 并验证类型。
令牌类型不是 refresh 时抛出 JWTError。
"""
payload = decode_token(token)
if payload.get("type") != "refresh":
raise JWTError("令牌类型不是 refresh")
return payload