"""附件直链的短期签名。
为什么需要它: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)