"""附件直链的短期签名。 为什么需要它:Markdown 里的图片是 ``,浏览器不会给它加 Authorization 头, 所以图片地址必须自带凭据。把**登录令牌**塞进 query 是最省事的做法,但代价明确: 令牌会进 Nginx access log、浏览器历史、以及任何复制粘贴出去的链接里,有效期还长达 30 分钟。 这里换成一个只为"这一张图"服务、只活 30 分钟的签名: `?exp=&sig=`。它不能用来调任何别的接口,泄漏了也只影响一张图的一小段时间。 """ import hashlib import hmac import time import uuid from app.config import get_settings # 域分离:即使有人拿 JWT 密钥来试,也拿不到同样的签名(反之亦然) _DOMAIN = b"attachment-raw-v1" def _key() -> bytes: base = get_settings().jwt_secret.encode("utf-8") return hmac.new(base, _DOMAIN, hashlib.sha256).digest() def _digest(workspace_id: uuid.UUID, attachment_id: uuid.UUID, exp: int) -> str: # 工作区也签进去,避免拿 A 工作区的签名去读 B 工作区的文件 payload = f"{workspace_id}:{attachment_id}:{exp}".encode() return hmac.new(_key(), payload, hashlib.sha256).hexdigest()[:32] def sign(workspace_id: uuid.UUID, attachment_id: uuid.UUID, ttl_seconds: int | None = None) -> str: """返回可直接拼在 `/raw` 后面的查询串。""" ttl = ttl_seconds if ttl_seconds is not None else get_settings().attachment_url_ttl_seconds exp = int(time.time()) + ttl return f"exp={exp}&sig={_digest(workspace_id, attachment_id, exp)}" def verify(workspace_id: uuid.UUID, attachment_id: uuid.UUID, exp: int | None, sig: str | None) -> bool: if not exp or not sig: return False if exp < int(time.time()): # 过期就是过期,不给宽限 return False return hmac.compare_digest(_digest(workspace_id, attachment_id, exp), sig)