Files
banban/talkingq-url/api/auth.py
2026-03-24 15:04:36 +08:00

251 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import Depends, HTTPException, status, Request, APIRouter, Path, Query
from fastapi.security import APIKeyHeader
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any, Tuple
from services.device_auth_manager import device_auth_manager
from utils.logger import session_logger
from config import settings
import re
router = APIRouter(prefix="/api/auth", tags=["Auth"])
device_id_header = APIKeyHeader(name="X-Device-ID", auto_error=False)
device_serial_header = APIKeyHeader(name="X-Device-Serial", auto_error=False)
admin_api_key_header = APIKeyHeader(name="X-Admin-API-Key", auto_error=False)
client_api_key_header = APIKeyHeader(name="X-Client-Key", auto_error=False)
async def verify_device(
device_id: str = Depends(device_id_header),
serial_number: str = Depends(device_serial_header)
):
"""验证设备ID和序列号"""
if not device_id or not serial_number:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少设备认证信息",
headers={"WWW-Authenticate": "DeviceAuth"},
)
is_valid = await device_auth_manager.authenticate_device(device_id, serial_number)
if not is_valid:
session_logger.warning("system", "auth", f"设备 {device_id} 认证失败")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="设备认证失败",
headers={"WWW-Authenticate": "DeviceAuth"},
)
return device_id
async def api_auth(
device_id: str = Depends(verify_device)
):
"""API认证依赖项只使用设备ID和序列号认证"""
return device_id
async def admin_auth(api_key: str = Depends(admin_api_key_header)) -> bool:
"""验证管理员API密钥"""
if not api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少管理员API密钥",
headers={"WWW-Authenticate": "AdminAuth"},
)
return api_key == settings.admin_api_key
async def client_auth(client_api_key: str = Depends(client_api_key_header)) -> bool:
"""验证小程序客户端API密钥"""
if not client_api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少客户端API密钥",
headers={"WWW-Authenticate": "ClientAuth"},
)
if client_api_key != settings.client_api_key:
session_logger.warning("system", "auth", "客户端API密钥验证失败")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="客户端API密钥无效",
headers={"WWW-Authenticate": "ClientAuth"},
)
return True
async def admin_or_api_auth(
device_id: str = Path(...),
admin_api_key: str = Depends(admin_api_key_header),
authenticated_device_id: str = Depends(api_auth)
) -> Tuple[str, bool]:
"""允许管理员或设备本身访问,返回(认证设备ID, 是否管理员)"""
is_admin = admin_api_key == settings.admin_api_key if admin_api_key else False
if not is_admin and authenticated_device_id != device_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="没有权限查询其他设备",
headers={"WWW-Authenticate": "DeviceAuth"},
)
return authenticated_device_id, is_admin
class DeviceRegistrationRequest(BaseModel):
device_id: str = Field(..., min_length=16, max_length=64, description="设备ID基于ESP32的MAC地址生成")
serial_number: str = Field(..., min_length=18, max_length=64, description="序列号,包含批次前缀和唯一码")
batch_id: Optional[str] = Field(None, description="批次ID使用YYYYMMDD格式")
is_active: bool = Field(True, description="设备激活状态")
class DeviceRegistrationResponse(BaseModel):
status: str
device_id: str
serial_number: str
batch_id: Optional[str] = None
is_active: bool
@router.post("/register-device", response_model=DeviceRegistrationResponse)
async def register_device(
request: DeviceRegistrationRequest,
admin_authenticated: bool = Depends(admin_auth)
):
"""注册新设备到认证白名单"""
batch_id = request.batch_id
if not batch_id and len(request.serial_number) >= 8:
batch_id = request.serial_number[:8]
success = await device_auth_manager.register_device(
request.device_id,
request.serial_number,
batch_id,
request.is_active
)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="设备注册失败",
)
return {
"status": "success",
"device_id": request.device_id,
"serial_number": request.serial_number,
"batch_id": batch_id,
"is_active": request.is_active
}
class BatchDeviceRegistrationRequest(BaseModel):
devices: List[DeviceRegistrationRequest]
class BatchDeviceRegistrationResponse(BaseModel):
status: str
registered_count: int
failed_count: int
details: List[Dict[str, Any]]
@router.post("/register-devices-batch", response_model=BatchDeviceRegistrationResponse)
async def register_devices_batch(
request: BatchDeviceRegistrationRequest,
admin_authenticated: bool = Depends(admin_auth)
):
"""批量注册设备到认证白名单"""
results = []
success_count = 0
failed_count = 0
for device in request.devices:
batch_id = device.batch_id
if not batch_id and len(device.serial_number) >= 8:
batch_id = device.serial_number[:8]
success = await device_auth_manager.register_device(
device.device_id,
device.serial_number,
batch_id,
device.is_active
)
if success:
success_count += 1
results.append({
"status": "success",
"device_id": device.device_id,
"serial_number": device.serial_number,
"batch_id": batch_id
})
else:
failed_count += 1
results.append({
"status": "failed",
"device_id": device.device_id,
"serial_number": device.serial_number
})
return {
"status": "completed",
"registered_count": success_count,
"failed_count": failed_count,
"details": results
}
@router.get("/verify-device/{device_id}")
async def verify_device(
device_id: str = Path(...),
serial_number: str = None,
auth_result: Tuple[str, bool] = Depends(admin_or_api_auth)
):
"""验证设备是否已正确注册 (管理员或设备自身可使用)"""
authenticated_device_id, is_admin = auth_result
if is_admin and serial_number:
is_valid = await device_auth_manager.authenticate_device(device_id, serial_number)
elif not is_admin:
is_valid = True
else:
is_valid = None
device_info = await device_auth_manager.get_device_info(device_id)
return {
"device_id": device_id,
"is_valid": is_valid,
"device_info": device_info
}
class SerialQueryResponse(BaseModel):
device_id: str
serial_number: str
@router.get("/query-serial", response_model=SerialQueryResponse)
async def query_serial_by_mac(
mac_address: str = Query(..., description="设备MAC地址"),
client_authenticated: bool = Depends(client_auth)
):
"""通过MAC地址查询设备ID和序列号"""
mac_pattern = re.compile(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$')
if not mac_pattern.match(mac_address):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="MAC地址格式无效应为XX:XX:XX:XX:XX:XX或XX-XX-XX-XX-XX-XX格式"
)
normalized_mac = mac_address.replace(":", "").replace("-", "").upper()
expected_device_id = f"TalkingQ_{normalized_mac}"
device_info = await device_auth_manager.get_device_info(expected_device_id)
if not device_info or not device_info.get("is_active", False):
session_logger.warning("system", "auth", f"未找到MAC地址{mac_address}对应的设备或设备未激活")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="未找到对应的设备信息或设备未激活"
)
return {
"device_id": expected_device_id,
"serial_number": device_info["serial_number"]
}
@router.get("/verify")
async def verify_auth(device_id: str = Depends(api_auth)):
"""验证设备认证状态"""
return {"status": "authenticated", "device_id": device_id}