静默监控的工程实践:Cookie 鉴权、动态 CSRF 与「只在变化时通知」的设计
前言
很多同学做"监控/签到/采集"这类自动化时,第一版往往是「定时跑一次,把结果推给我」。跑上几天就会发现两个问题:一是告警疲劳(没变化也一直在推,最后你会把通知关掉);二是跑了一段时间突然失效(Cookie 或 CSRF 令牌过期,脚本静默失败,你却不知道)。
本文把我在一台 NAS 上做的长期静默监控实践中踩到的坑和最终结构整理出来,涉及 Cookie 鉴权、动态 CSRF、调度粒度和"只推变化"的通知设计。所有代码片段都是真实在跑的简化版(Python 3,只用标准库)。
一、先说结论:什么叫"静默监控"
- 无变化 = 不通知。只有状态真的变了才发消息,这是让通知活得久的前提。
- 失败了要吵。反过来,鉴权失效/接口异常必须主动报警,否则你会以为"一切正常"。
- 告警要分优先级,并且明确"什么不推"。例如库存监控里,"取消订单退回 1 件"这种噪音就不该推。
实现上就是一句话:保存上一次的快照,每次拉取后做 diff。
prev = state.get("goods") or {}
cur = {g["id"]: normalize(g) for g in fetch()}
if not prev:
save(cur) # 首次只建基线,不通知
else:
changes = diff(cur, prev)
if changes:
notify(changes) # 有变化才推
save(cur)
二、鉴权:Cookie 的坑,90% 的人第一步就错
抓包抄 Cookie 是最常见的做法,但有个细节几乎人人都踩:
❌ 只抄
document.cookie或用 Cookie 导出插件 → 一定失败。
✅ 从 DevTools 的 Network 面板里,复制某个请求的Cookie:请求头,或者直接 Copy as cURL。
原因是登录令牌通常是 HttpOnly 的,JS 读不到,导出插件默认也不导。症状很有迷惑性:你明明在浏览器里登录着,脚本却一直返回"未登录"。
经验的判别方式:如果服务端返回 401/403 not login,而你的 Cookie 串里找不到类似 SID、xxx_token、session 这类明显的会话项,基本就是这个原因。
三、CSRF:令牌会过期,必须动态取
很多系统(尤其是背后有网关的平台)除了 Cookie,还要求一个 CSRF 令牌放在请求头里。关键点是:这个令牌有寿命,通常几十分钟。写死一个值,脚本跑一会儿就会开始 403。
典型表现:
# 不带令牌
403 {"message":"no request_csrf found"}
# 带了过期的令牌
403 {"message":"CSRF is nil"}
正确做法是每次请求前确保令牌新鲜,并且失败时能自动刷新重试一次:
_CSRF_CACHE = {}
def get_csrf(cookie, refresh=False):
if not refresh and _CSRF_CACHE.get(cookie[-40:]):
return _CSRF_CACHE[cookie[-40:]]
# 令牌由专门的接口通过「响应头」下发
r = urllib.request.Request(CSRF_URL, headers={"Cookie": cookie})
with urllib.request.urlopen(r, timeout=20) as resp:
tok = resp.headers.get("csrf") # ← 注意是响应头
if tok:
_CSRF_CACHE[cookie[-40:]] = tok
return tok
def request(url, body=None, _retry=True):
headers = {"Cookie": cookie, "csrf": get_csrf(cookie),
"Content-Type": "application/json"}
try:
... # 正常请求
except urllib.error.HTTPError as e:
text = e.read().decode()
if _retry and "csrf" in text.lower(): # 令牌过期 → 刷新重试一次
get_csrf(cookie, refresh=True)
return request(url, body, _retry=False)
raise
注意:令牌往往不在 Cookie 里,而是某个接口的响应头里(本文这套是 GET /api/get-ainfo 返回的 csrf 响应头)。所以别拿 Cookie 里名字很像的那个值去凑。
四、调度粒度:cron 最小 1 分钟,但你需要 5 秒
想要"更快的响应",用 cron 是压不下去的(最小粒度 1 分钟)。一个简单可靠的折中是:cron 每分钟拉起一次,脚本在自己的进程里做高频轮询,跑满约 50 秒后退出。
# cron: * * * * *
POLL_INTERVAL = 5 # 轮内每 5 秒探一次
RUN_SECONDS = 55 # 单次运行时长,留出空隙交给下一次 cron
deadline = time.time() + RUN_SECONDS
while time.time() < deadline:
t0 = time.time()
try:
poll_once() # 一次内只发 1 个请求
except Exception as e:
log("本轮异常", e) # 单轮失败不影响后续
sleep = POLL_INTERVAL - (time.time() - t0)
if time.time() + max(sleep, 0) >= deadline:
break
time.sleep(max(sleep, 0))
两个必须配的护栏:
- 文件锁防重叠:万一某轮耗时超过 60 秒,下一次 cron 已经在路上了,没有锁就会出现两个进程同时跑、状态互相覆盖。
- 退避重试:被限流(429/503)时不要死磕,指数退避。
LOCK = os.path.join(HERE, ".monitor.lock")
fd = os.open(LOCK, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
# ... finally: os.close(fd); os.unlink(LOCK)
五、把"有用信息"放进通知里
通知只推变化还不够,内容也要直接可用。以库存监控为例,每条消息里都带上:当前是否可购买、价格、总量/已售/剩余、以及一张"当前可购买清单"。这样用户扫一眼就能决策,不用再打开 App 查。
另外建议把关键账号状态一并带上(比如积分余额、剩余可用次数),因为这类自动化最终往往要落到"能不能真的执行一次"上。
六、一个反面提醒:别为了跑通而绕过平台规则
实践中经常遇到"只差一个身份/权限"的情况。此时如果想的是"找接口把这个身份开出来",那就越界了:这类接口通常是给演示/测试账号用的,用它给你的真实账号开权限,风险是账号被处置。
能做的是把链路摸清、把代价讲明,然后把选择权交回给人。技术上的"可做"和"该做"是两件事。
七、小结 Checklist
- Cookie 从
Network → 请求头或 Copy as cURL 取,别用导出插件(HttpOnly)。 - CSRF/签名类令牌动态获取,缓存 + 过期自动刷新重试。
- 快照 + diff 决定"推不推",无变化一律静默。
- 鉴权失效、接口异常必须报警(这类"静默失败"最致命)。
- 调度用"cron 每分钟拉起 + 进程内高频轮询",配文件锁与退避。
- 不在通知里堆原始数据,直接给结论与可执行信息。
把这几点做到,一个自动化脚本从"能跑"到"能长期跑",差距基本就在这里。
- 点赞
- 收藏
- 关注作者
评论(0)