Files

257 lines
7.5 KiB
Python
Raw Permalink Normal View History

"""
验证码和 Cookie 策略管理模块
提供功能:
1. 图形验证码生成与验证 API
2. 现代 Cookie 策略管理 (HttpOnly, Secure, SameSite)
3. 验证码结果持久化到 Cookie
"""
import random
import string
import json
from typing import Optional
from fastapi import APIRouter, HTTPException, Request, Response
from fastapi.responses import JSONResponse
router = APIRouter(prefix="/captcha", tags=["验证码"])
# ==================== 数据模型 ====================
class CaptchaConfig:
"""验证码配置"""
LENGTH = 6 # 验证码长度
CHARSET = string.ascii_letters + string.digits # 字符集: 大小写字母+数字
EXPIRE_SECONDS = 3600 # 过期时间: 1小时
class CaptchaResult:
"""验证码结果"""
def __init__(self, text: str):
self.text = text
self.created_at = int(__import__('time').time())
@property
def is_expired(self) -> bool:
now = int(__import__('time').time())
return (now - self.created_at) > CaptchaConfig.EXPIRE_SECONDS
# ==================== 全局状态 ====================
# 内存中的验证码存储 (生产环境建议用 Redis)
_active_captchas: dict[str, CaptchaResult] = {}
# ==================== 验证码 API ====================
@router.get("/generate", summary="生成新验证码")
async def generate_captcha(
response: Response,
use_cookie: bool = False, # 是否通过 Cookie 传递验证码文本
length: int = CaptchaConfig.LENGTH,
):
"""
生成新的验证码
- **use_cookie**: 是否同时设置 Cookie (方便前端读取)
- **length**: 验证码长度 (4-10)
返回:
- **request_id**: 验证码请求 ID
- **expires_in**: 过期时间(秒)
"""
# 生成随机字符串
chars = CaptchaConfig.CHARSET
captcha_text = ''.join(random.choices(chars, k=length))
# 存储到内存
request_id = f"captcha_{int(__import__('time').time() * 1000)}"
_active_captchas[request_id] = CaptchaResult(captcha_text)
# 如果请求使用 Cookie,设置 HttpOnly Cookie
if use_cookie:
response.set_cookie(
key="llm_captcha_text",
value=captcha_text,
max_age=CaptchaConfig.EXPIRE_SECONDS,
httponly=False, # 允许前端读取
secure=False, # HTTP/HTTPS 都适用
samesite="Lax", # 防止 CSRF
domain=".imageteach.tech",
path="/"
)
return {
"request_id": request_id,
"expires_in": CaptchaConfig.EXPIRE_SECONDS,
"cookie_set": use_cookie
}
@router.post("/validate", summary="验证用户输入的验证码")
async def validate_captcha(
request: Request,
user_input: str,
request_id: Optional[str] = None,
):
"""
验证用户输入的验证码
- **user_input**: 用户输入的验证码文本
- **request_id**: 可选,指定验证哪个验证码
返回:
- **is_valid**: 是否验证成功
- **submitted**: 用户提交的文本
"""
if not user_input:
raise HTTPException(status_code=400, detail="缺少验证码输入")
# 从请求头或 Cookie 获取 request_id
rid = request_id or request.headers.get("X-Captcha-Request-Id")
if not rid or rid not in _active_captchas:
raise HTTPException(
status_code=404,
detail="未找到验证码,请先生成"
)
captcha_result = _active_captchas[rid]
# 检查是否过期
if captcha_result.is_expired:
del _active_captchas[rid]
raise HTTPException(
status_code=410, # Gone
detail="验证码已过期,请重新生成"
)
# 不区分大小写比较
is_valid = captcha_result.text.lower() == user_input.strip().lower()
# 验证成功后删除该验证码 (一次性使用)
if is_valid:
del _active_captchas[rid]
return {
"is_valid": is_valid,
"submitted": user_input,
"matched": is_valid
}
@router.delete("/clear", summary="清除验证码 Cookie")
async def clear_captcha_cookie(response: Response):
"""清除所有验证码相关的 Cookie"""
response.delete_cookie(key="llm_captcha_text")
response.delete_cookie(key="llm_captcha_result")
return {"message": "验证码 Cookie 已清除"}
# ==================== Cookie 策略工具类 ====================
class CookiePolicy:
"""
现代 Cookie 策略管理器
支持的属性:
- **HttpOnly**: 防止 XSS 读取 Cookie
- **Secure**: 仅 HTTPS 传输 (当前设为 False 以支持 HTTP)
- **SameSite**: Lax/Strict/None (控制跨域行为)
- **Domain**: 指定域名 (.imageteach.tech)
- **Path**: 路径 (/)
- **Max-Age**: 过期时间 (秒)
"""
# 默认 Cookie 配置
DEFAULT_CONFIG = {
"llm_session": {
"max_age": 86400 * 7, # 7天
"httponly": True, # 防止 XSS
"secure": False, # HTTP/HTTPS 都适用
"samesite": "Lax", # 防止 CSRF
"domain": ".imageteach.tech",
"path": "/"
},
"llm_captcha": {
"max_age": 3600, # 1小时
"httponly": False,
"secure": False,
"samesite": "Lax",
"domain": ".imageteach.tech",
"path": "/"
},
"llm_preferences": {
"max_age": 86400 * 30, # 30天
"httponly": False,
"secure": True,
"samesite": "None", # 跨域场景
"domain": ".imageteach.tech",
"path": "/"
}
}
@classmethod
def set_cookie(cls, response: Response, name: str, value: str, override: dict = None):
"""
设置 Cookie
Args:
response: FastAPI Response 对象
name: Cookie 名称
value: Cookie 值
override: 可选的覆盖配置
"""
config = cls.DEFAULT_CONFIG.get(name, {})
if override:
config.update(override)
response.set_cookie(
key=name,
value=value,
max_age=config.get("max_age", 3600),
httponly=config.get("httponly", False),
secure=config.get("secure", False),
samesite=config.get("samesite", "Lax"),
domain=config.get("domain", ".imageteach.tech"),
path=config.get("path", "/")
)
@classmethod
def get_cookie_config(cls, name: str) -> dict:
"""获取 Cookie 配置"""
return cls.DEFAULT_CONFIG.get(name, {})
# ==================== 前端可用的 API ====================
@router.get("/cookies/list", summary="列出所有验证码相关 Cookie")
async def list_captcha_cookies(request: Request):
"""返回当前请求携带的所有验证码相关 Cookie"""
cookies = {
k: v for k, v in request.cookies.items()
if k.startswith("llm_")
}
return {
"cookies": cookies,
"has_captcha": "llm_captcha_text" in cookies,
"has_session": "llm_session" in cookies
}
@router.post("/cookies/set", summary="设置测试 Cookie")
async def set_test_cookie(
response: Response,
cookie_name: str = "llm_test",
cookie_value: str = "test_value"
):
"""设置一个测试用的 Cookie"""
CookiePolicy.set_cookie(response, cookie_name, cookie_value)
return {
"message": f"Cookie '{cookie_name}' 已设置",
"name": cookie_name,
"value": cookie_value
}