# ============================================================================= # 企微IT智能服务台 — 头像代理 API # ============================================================================= # 说明:代理企微头像图片,解决 COEP/Mixed Content/CSP 跨域问题。 # 前端通过 /api/avatar/proxy?url= 访问企微头像, # 后端服务器请求图片并返回,避免浏览器直接请求 wework.qpic.cn。 # ============================================================================= import logging from urllib.parse import unquote import httpx from fastapi import APIRouter, Query, Response from app.utils.response import AppException logger = logging.getLogger(__name__) router = APIRouter() # 允许代理的域名白名单 ALLOWED_AVATAR_DOMAINS = ["wework.qpic.cn"] @router.get("/avatar/proxy") async def proxy_avatar( url: str = Query(..., description="要代理的头像 URL(URL 编码)"), ): """代理企微头像图片。 解决浏览器 COEP/CSP/Mixed Content 策略导致 wework.qpic.cn 图片无法加载的问题。 后端服务器请求图片后返回给前端,代理 URL 为同源 HTTPS,不受跨域策略限制。 Args: url: 要代理的头像 URL(URL 编码后的) Returns: Response: 图片二进制数据,带 Content-Type 和 Cache-Control 头 Raises: AppException: URL 不在白名单中或图片获取失败 """ # 解码 URL avatar_url = unquote(url) # 安全校验:只允许代理白名单域名的图片 if not any(domain in avatar_url for domain in ALLOWED_AVATAR_DOMAINS): logger.warning(f"头像代理被拒绝(域名不在白名单): {avatar_url[:80]}") raise AppException(code=403, message="只允许代理企微头像") # 确保 HTTPS if avatar_url.startswith("http://"): avatar_url = "https://" + avatar_url[7:] # 请求图片 try: async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: resp = await client.get(avatar_url) if resp.status_code == 200: content_type = resp.headers.get("content-type", "image/jpeg") return Response( content=resp.content, media_type=content_type, headers={ "Cache-Control": "public, max-age=86400", # 浏览器缓存 1 天 }, ) else: logger.warning( f"头像代理获取失败: url={avatar_url[:80]}, status={resp.status_code}" ) raise AppException(code=404, message="头像获取失败") except httpx.TimeoutException: logger.warning(f"头像代理超时: url={avatar_url[:80]}") raise AppException(code=504, message="头像获取超时") except Exception as e: logger.warning(f"头像代理异常: url={avatar_url[:80]}, error={e}") raise AppException(code=500, message="头像代理失败")