fix(订阅式软件提供): 远端资源改 no-cache 条件校验,且 ETag 按 RFC7232 弱比较
- 浏览器侧 max-age=24h 会把旧产物挡住一天,提供者重建后被浏览器旧副本劫持;改为 no-cache + ETag:每次校验、未变 304,回源仍受主服务 24h TTL 限制 - 实测响应 ETag 被网关降为弱校验(W/ 前缀),严格相等匹配拿不到 304;改为归一化(去 W/ 与引号)后按弱比较命中,实测 304
This commit is contained in:
1 parent
f812413de5
commit
6165df5d78
1 file changed
+29
-6
@@ -45,11 +45,34 @@ from gvsdsdk.model_utils import UUIDObj
|
||||
from gvsds.apps.gerp.app_manager import AppManager
|
||||
from gvsds.apps.gerp.views import ELTAuthenticatedAPIView
|
||||
|
||||
from .manager import REMOTE_ASSET_TTL_SECONDS, SubscriptionManager
|
||||
from .manager import SubscriptionManager
|
||||
from .permissions import IsSubscriptionProvider, SubscriptionProviderAuthentication
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: 远端资源下发给浏览器的缓存策略
|
||||
# 「可存但每次校验」= 浏览器每次都带 If-None-Match 回主服务,内容未变即 304:
|
||||
# - 提供者重建后立即生效(不会像 max-age=24h 那样被浏览器旧副本挡住)
|
||||
# - 回源(打子服务器)仍受主服务 24h TTL 限制,负载不增加
|
||||
_REMOTE_ASSET_CACHE_CONTROL = 'no-cache'
|
||||
|
||||
|
||||
def _NormalizeEtag(value):
|
||||
"""归一化 ETag:去掉弱校验前缀 W/ 与两侧引号(RFC 7232 弱比较口径)"""
|
||||
return (value or '').strip().removeprefix('W/').strip().strip('"')
|
||||
|
||||
|
||||
def _EtagMatches(HeaderValue, Etag):
|
||||
"""If-None-Match 是否命中给定 ETag
|
||||
|
||||
RFC 7232:If-None-Match 用**弱比较**(忽略 W/ 前缀与引号);上游/网关可能把
|
||||
我们的强 ETag 改成弱 ETag(实测响应是 W/"<sha256>"),故必须归一化后比较。
|
||||
"""
|
||||
if not HeaderValue:
|
||||
return False
|
||||
target = _NormalizeEtag(Etag)
|
||||
return any(_NormalizeEtag(item) == target for item in HeaderValue.split(','))
|
||||
|
||||
|
||||
def _ToBytes(value):
|
||||
if value is None:
|
||||
@@ -430,8 +453,8 @@ class SubscriptionRemoteAssetView(APIView):
|
||||
GET /API/Subscription/RemoteAsset/<SubscriptionGroupUUID>/<AssetPath>
|
||||
|
||||
- 缓存:内容落库,TTL 24 小时;过期回源并按内容 SHA256 判断是否变更
|
||||
- 浏览器侧同为 24h(`Cache-Control: public, max-age=86400`),并带 ETag(SHA256):
|
||||
过期/硬刷新后走条件请求,内容未变即 304,不回源
|
||||
- 下发策略 `no-cache` + ETag(SHA256):浏览器每次校验、未变即 304(不回源),
|
||||
因此提供者重建后立即生效,而回源频率上限仍是 24h
|
||||
- 免登录:与子服务器原本的公开资源一致(页面渲染前浏览器无法携带业务鉴权)
|
||||
"""
|
||||
|
||||
@@ -446,15 +469,15 @@ class SubscriptionRemoteAssetView(APIView):
|
||||
err, status=502, content_type='text/plain; charset=utf-8')
|
||||
|
||||
etag = f'"{payload["SHA256"]}"'
|
||||
if payload['SHA256'] and request.headers.get('If-None-Match') == etag:
|
||||
if payload['SHA256'] and _EtagMatches(request.headers.get('If-None-Match'), etag):
|
||||
response = HttpResponse(status=304)
|
||||
response['ETag'] = etag
|
||||
response['Cache-Control'] = _REMOTE_ASSET_CACHE_CONTROL
|
||||
return response
|
||||
|
||||
response = HttpResponse(payload['Content'], content_type=payload['ContentType'])
|
||||
response['ETag'] = etag
|
||||
# 主服务侧与浏览器侧同为 24h(ETag 保留:硬刷新/过期后仍走条件请求,命中即 304)
|
||||
response['Cache-Control'] = f'public, max-age={REMOTE_ASSET_TTL_SECONDS}'
|
||||
response['Cache-Control'] = _REMOTE_ASSET_CACHE_CONTROL
|
||||
response['X-Remote-Cache'] = 'HIT' if payload['Hit'] else ('CHANGED' if payload['Changed'] else 'FRESH')
|
||||
return response
|
||||
|
||||
|
||||
Reference in new issue
Block a user