33import ipaddress
44import socket
55import threading
6- import time
76from hashlib import sha256
87from pathlib import Path
98from typing import Dict , Iterable , List , Optional , Set , Union
@@ -57,7 +56,6 @@ def _resolve_addrinfo_to_ips(
5756
5857class SecurityUtils :
5958 _SIGNED_URL_PURPOSE = "image-proxy"
60- _SIGNED_URL_EXPIRE_SECONDS = 86400
6159
6260 @staticmethod
6361 def is_safe_path (base_path : Path , user_path : Path ,
@@ -377,23 +375,26 @@ async def _is_allowed_private_hostname_async(
377375 )
378376
379377 @staticmethod
380- def _url_signature_payload (url : str , expires_at : int , purpose : str ) -> bytes :
378+ def _url_signature_payload (url : str , purpose : str ) -> bytes :
381379 """
382380 构造 URL 签名载荷。
383381
384- 签名覆盖用途、过期时间和完整 URL,确保同一个签名不能挪用到其它
385- 内网地址或其它代理用途。
382+ 签名覆盖用途与完整 URL,确保同一个签名不能挪用到其它代理用途或其它 URL。
386383 """
387- return f"{ purpose } \n { expires_at } \n { url } " .encode ("utf-8" )
384+ return f"{ purpose } \n { url } " .encode ("utf-8" )
388385
389386 @staticmethod
390- def _sign_url_payload (url : str , expires_at : int , purpose : str ) -> str :
387+ def _sign_url_payload (url : str , purpose : str ) -> str :
391388 """
392389 使用 RESOURCE_SECRET_KEY 对 URL 签名载荷生成 HMAC。
390+
391+ 相同 `(url, purpose, RESOURCE_SECRET_KEY)` 组合在进程生命周期内输出
392+ 完全一致;签名的失效边界绑定在 `RESOURCE_SECRET_KEY` 上,进程重启
393+ 或显式轮换密钥时所有旧签名一起作废。
393394 """
394395 return hmac .new (
395396 settings .RESOURCE_SECRET_KEY .encode ("utf-8" ),
396- SecurityUtils ._url_signature_payload (url , expires_at , purpose ),
397+ SecurityUtils ._url_signature_payload (url , purpose ),
397398 sha256 ,
398399 ).hexdigest ()
399400
@@ -413,26 +414,29 @@ def strip_url_signature(url: str) -> str:
413414 @staticmethod
414415 def sign_url (
415416 url : str ,
416- expires_in : int = _SIGNED_URL_EXPIRE_SECONDS ,
417417 purpose : str = _SIGNED_URL_PURPOSE ,
418418 ) -> str :
419419 """
420- 给服务端返回的资源 URL 添加临时签名。
420+ 给服务端返回的资源 URL 添加稳定签名。
421+
422+ 签名作为 `/system/img` 代理放行私网图片 URL 的能力凭证:图片代理默认
423+ 拒绝解析到非公网地址的 URL(防 SSRF),合法媒体服务器 URL 必须由后端
424+ 预先签名后才能跳过该限制。
421425
422- 该签名用于允许 `/system/img` 代理访问服务端已经确认过的私网图片 URL,
423- 避免代理端点重新依赖媒体服务器的具体路径规则。
426+ 签名为 `(url, purpose, RESOURCE_SECRET_KEY)` 的确定性 HMAC,**不带
427+ 过期时间**:相同 URL 多次调用结果完全一致,让浏览器与 Service Worker
428+ 的缓存能稳定命中;失效边界由 `RESOURCE_SECRET_KEY` 控制——进程重启
429+ 自动重生成、或者运维显式轮换后所有历史签名一起作废。
424430 """
425431 if not url :
426432 return url
427433 parsed_url = urlparse (url )
428434 if parsed_url .scheme not in {"http" , "https" } or not parsed_url .netloc :
429435 return url
430436 clean_url = SecurityUtils .strip_url_signature (url )
431- expires_at = int (time .time () + expires_in )
432- signature = SecurityUtils ._sign_url_payload (clean_url , expires_at , purpose )
437+ signature = SecurityUtils ._sign_url_payload (clean_url , purpose )
433438 fragment = urlencode (
434439 {
435- "mp_exp" : str (expires_at ),
436440 "mp_sig" : signature ,
437441 "mp_purpose" : purpose ,
438442 }
@@ -446,29 +450,23 @@ def verify_signed_url(
446450 ) -> Optional [str ]:
447451 """
448452 验证 URL fragment 中的代理签名,成功时返回去签名后的真实 URL。
453+
454+ 签名只校验 `(url, purpose, RESOURCE_SECRET_KEY)`,密钥轮换/进程重启
455+ 后旧签名自动失效。
449456 """
450457 if not url :
451458 return None
452459 parsed_url = urlparse (url )
453460 if parsed_url .scheme not in {"http" , "https" } or not parsed_url .netloc :
454461 return None
455462 fragment_params = dict (parse_qsl (parsed_url .fragment , keep_blank_values = True ))
456- expires_at = fragment_params .get ("mp_exp" )
457463 signature = fragment_params .get ("mp_sig" )
458464 signed_purpose = fragment_params .get ("mp_purpose" )
459- if not expires_at or not signature or signed_purpose != purpose :
460- return None
461- try :
462- expires_at_int = int (expires_at )
463- except ValueError :
464- return None
465- if expires_at_int < int (time .time ()):
465+ if not signature or signed_purpose != purpose :
466466 return None
467467
468468 clean_url = SecurityUtils .strip_url_signature (url )
469- expected_signature = SecurityUtils ._sign_url_payload (
470- clean_url , expires_at_int , purpose
471- )
469+ expected_signature = SecurityUtils ._sign_url_payload (clean_url , purpose )
472470 if not hmac .compare_digest (signature , expected_signature ):
473471 return None
474472 return clean_url
0 commit comments