add banbanmini backend
This commit is contained in:
250
talkingq-url/api/auth.py
Normal file
250
talkingq-url/api/auth.py
Normal file
@@ -0,0 +1,250 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user