Files
WonderQ-Project/WonderQ-Admin/app/media_urls.py
duanshuwen 6245159e7c feat: 添加后台登录验证码、记住密码功能,优化媒体资源与前端规范
- 新增后台登录图形验证码功能,完善登录安全防护
- 新增登录rememberMe参数,控制Refresh Token的会话持久化策略
- 实现OSS私有桶媒体URL自动签名,统一处理图片资源的临时访问签名
- 新增素材库数据库表与上传API,规范媒体资源管理流程
- 统一前端UI图标使用@element-plus/icons-vue,重构布局图标组件
- 登录页新增验证码输入、刷新功能,添加账号记忆与记住密码逻辑
- 更新全套文档,补充API契约、技术决策记录与集成流程说明
- 修复多个业务页面的图标展示问题,新增认证流程相关测试用例
2026-08-27 07:50:59 +08:00

91 lines
2.6 KiB
Python

import base64
import hashlib
import hmac
from time import time
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from .config import get_settings
SIGNED_QUERY_KEYS = {"OSSAccessKeyId", "Expires", "Signature"}
MEDIA_URL_EXPIRES_SECONDS = 3600
def normalized_oss_host(endpoint: str, bucket: str) -> tuple[str, str]:
raw = endpoint.strip().rstrip("/")
if "://" not in raw:
raw = f"https://{raw}"
parsed = urlsplit(raw)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("invalid OSS endpoint")
host = parsed.netloc
if not host.lower().startswith(f"{bucket.lower()}."):
host = f"{bucket}.{host}"
return parsed.scheme, host
def sign_oss_get_url(
url: str,
*,
access_key_id: str,
access_key_secret: str,
endpoint: str,
bucket: str,
expires_at: int,
) -> str:
scheme, expected_host = normalized_oss_host(endpoint, bucket)
parsed = urlsplit(url)
if parsed.scheme != scheme or parsed.netloc.lower() != expected_host.lower():
return url
path = parsed.path or "/"
canonical_resource = f"/{bucket}{path}"
string_to_sign = f"GET\n\n\n{expires_at}\n{canonical_resource}"
signature = base64.b64encode(
hmac.new(
access_key_secret.strip().encode("utf-8"),
string_to_sign.encode("utf-8"),
hashlib.sha1,
).digest()
).decode("ascii")
query = [
(key, value)
for key, value in parse_qsl(parsed.query, keep_blank_values=True)
if key not in SIGNED_QUERY_KEYS
]
query.extend(
[
("OSSAccessKeyId", access_key_id.strip()),
("Expires", str(expires_at)),
("Signature", signature),
]
)
return urlunsplit((scheme, expected_host, path, urlencode(query), ""))
def resolve_media_url(url: str | None) -> str | None:
if not url or not isinstance(url, str):
return url
settings = get_settings()
values = (
settings.oss_access_key_id,
settings.oss_access_key_secret,
settings.oss_endpoint,
settings.oss_bucket_name,
)
if not all(value and value.strip() for value in values):
return url
try:
return sign_oss_get_url(
url,
access_key_id=settings.oss_access_key_id,
access_key_secret=settings.oss_access_key_secret,
endpoint=settings.oss_endpoint,
bucket=settings.oss_bucket_name,
expires_at=int(time()) + MEDIA_URL_EXPIRES_SECONDS,
)
except (TypeError, ValueError):
return url