CI / backend (push) Canceled after 0s
- README:富文本行改成「Web Vditor 编辑 / 安卓 Markwon 预览编辑」,e2e 15 项 - ROADMAP 第 5 节:勾掉表格与超过一两行的旧描述(表格是只读渲染, HTML 片段仍按文本显示);附件改为短期签名 raw_url;安卓从"未做"改成已完成 - 记下仍欠的三件事:保存后回列表而不是回详情、图片压缩/缩略图、离线缓存 验证:pytest 74 passed;richtext 15 / smoke 7 / tickets 8 / knowledge 7 全绿; tsc -b 0 错;vite build 通过;安卓 assembleDebug 与模拟器逐屏走通。
46 lines
1.9 KiB
Python
46 lines
1.9 KiB
Python
"""附件直链的短期签名。
|
|
|
|
为什么需要它:Markdown 里的图片是 `<img src="…">`,浏览器不会给它加 Authorization 头,
|
|
所以图片地址必须自带凭据。把**登录令牌**塞进 query 是最省事的做法,但代价明确:
|
|
令牌会进 Nginx access log、浏览器历史、以及任何复制粘贴出去的链接里,有效期还长达 30 分钟。
|
|
|
|
这里换成一个只为"这一张图"服务、只活 30 分钟的签名:
|
|
`?exp=<unix 秒>&sig=<hmac 前 32 位>`。它不能用来调任何别的接口,泄漏了也只影响一张图的一小段时间。
|
|
"""
|
|
|
|
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)
|