天津 RS 协议分析:页面初始化、动态 Cookie 与浏览器环境排查
补充小白从零开始的离线 Demo、运行命令、预期输出和从 Demo 过渡到项目代码的完整说明。
天津 RS 协议分析:页面初始化、动态 Cookie 与浏览器环境排查
这篇文章记录我整理天津电子税务页面 RS(瑞数)防护流程的过程。项目最初是为了理解:为什么同一个页面在浏览器中可以正常打开,而直接使用普通 HTTP 客户端时会返回拦截页、跳转页或空响应。
我把问题拆成了四层:
页面初始化
↓
服务端下发动态参数和脚本
↓
浏览器执行脚本并建立会话状态
↓
后续请求携带会话状态和正确的 URL 形态
RS 在这里不是单一的“加密算法”。它更接近一套由页面脚本、动态 Cookie、请求时序、浏览器环境和 URL 处理共同组成的访问控制机制。项目中既有浏览器自动化方案,也有使用 requests + execjs 分析页面脚本的纯协议实验。
本文只针对本人或所在组织已经获准测试的环境。文章里的域名、Cookie、路径和参数均为抽象示例,不包含真实访问凭据、设备指纹、动态 Cookie 生成结果或可直接复用的风控绕过流程。
一、项目文件
项目主要由三个文件组成:
天津RS协议/
├── 3_rs_request.py # 浏览器自动化方式的请求封装
├── 3_rs_protocol.py # requests + execjs 的协议分析实验
└── tianjin_rs_env.js # 为本地 JavaScript 执行准备的环境桩
两个 Python 文件的定位不同。
3_rs_request.py 使用真实浏览器加载页面,让页面自身执行 RS 脚本,然后在浏览器上下文中进行诊断。它的优点是浏览器环境完整,页面脚本可以直接访问真实的 window、document、navigator 和 Cookie API。
3_rs_protocol.py 则尝试把页面返回的内联脚本、外部脚本和环境桩拼在一起,交给 Node.js 执行。这种方式便于研究脚本逻辑,但需要不断补齐浏览器对象,也更容易因为版本变化失效。
二、RS 防护到底检查什么
我把项目运行时观察到的检查项整理成下面几类。
1. 初始页面材料
服务端返回的 HTML 中可能包含:
- 特殊
meta标签; - 内联初始化脚本;
- 外部 JavaScript 路径;
- 当前页面和会话相关的时间信息;
- 页面版本或环境参数。
这些内容通常不是最终业务数据,而是后续脚本运行所需要的输入。
2. 动态 Cookie
页面脚本执行后可能通过 document.cookie 写入新的 Cookie。Cookie 往往和当前页面、时间、脚本版本及浏览器环境有关,不能当作永久凭据保存或跨会话复用。
我在代码中只记录 Cookie 名称、数量和生命周期,不把完整值写入日志:
def cookie_summary(cookie_items):
return {
"count": len(cookie_items),
"names": sorted(
item.get("name", "")
for item in cookie_items
if item.get("name")
),
}
3. 浏览器环境
页面脚本可能读取:
navigator属性;- 屏幕尺寸和颜色深度;
- 当前 URL、Origin 和 Referer;
document状态;- 浏览器提供的 Web API;
- 脚本执行时序。
因此,“只复制一个 Cookie”通常不能完整复现浏览器请求。Cookie、页面脚本、浏览器环境和请求顺序是一个整体。
4. URL 和请求时序
有些 RS 版本不仅校验 Cookie,还会处理 URL 或查询参数。页面脚本可能在请求发出前追加动态参数,或者根据当前 URL 重新计算校验值。
这也是项目中普通 requests 请求比浏览器内 fetch 更容易失败的原因:浏览器请求使用的是页面运行时已经处理过的最终 URL,而普通客户端可能只拿到了原始路径。
三、第一步:只分析 HTML,不急着执行脚本
我先请求一个本地保存的 HTML 样例,检查是否存在 RS 相关的初始化结构。分析阶段只读取标签和脚本长度,不输出脚本全文。
from bs4 import BeautifulSoup
from urllib.parse import urljoin
def analyze_bootstrap(html: str, base_url: str) -> dict:
soup = BeautifulSoup(html, "html.parser")
metas = []
for tag in soup.find_all("meta"):
attrs = dict(tag.attrs)
if attrs.get("r") == "m" or "content" in attrs:
metas.append({
"name": attrs.get("name", ""),
"role": attrs.get("r", ""),
"content_length": len(attrs.get("content", "")),
})
inline_scripts = []
external_scripts = []
for tag in soup.find_all("script"):
src = tag.get("src")
if src:
external_scripts.append(urljoin(base_url, src))
else:
code = tag.string or tag.get_text() or ""
if code.strip():
inline_scripts.append({
"length": len(code),
"sha256": sha256_text(code),
})
return {
"meta_count": len(metas),
"metas": metas,
"inline_scripts": inline_scripts,
"external_scripts": external_scripts,
}
配套的摘要函数只用于比较脚本是否变化:
import hashlib
def sha256_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
我不会把完整脚本内容写入文章或日志。对脚本做 SHA-256 摘要以后,可以比较不同日期、不同环境返回的脚本是否发生变化,同时不泄露脚本中的动态材料。
四、第二步:确认脚本依赖
拿到 HTML 后,我会记录以下关系:
meta 参数
├── 被内联脚本读取
├── 参与外部脚本初始化
└── 影响 document.cookie 或请求参数
排查时使用关键词定位入口:
grep -nE "document\\.cookie|location|navigator|XMLHttpRequest|fetch|setTimeout" \
captured_page.html
对外部脚本只做离线分析:
def script_inventory(scripts):
result = []
for index, script in enumerate(scripts, 1):
result.append({
"index": index,
"url": script.get("url", ""),
"length": len(script.get("code", "")),
"sha256": sha256_text(script.get("code", "")),
})
return result
我重点查看脚本是否:
- 读取某个
meta的content; - 访问
document.cookie; - 读取
location.href或location.host; - 依赖
navigator或屏幕参数; - 使用
fetch、XHR 或 Beacon; - 通过定时器延迟执行;
- 根据页面路径生成不同结果。
这一步的目标是画出依赖图,而不是马上把所有代码翻译成 Python。
五、第三步:浏览器路线适合做什么
浏览器路线的价值是验证页面真实行为。真实浏览器会自动提供 DOM、Cookie、Web API 和脚本执行环境,适合确认:
- 页面是否能正常完成初始化;
- 动态 Cookie 是否在页面加载后出现;
- Cookie 的过期时间和作用域;
- 请求是在什么时候发出的;
- 页面脚本是否改写了 URL;
- 页面是否出现人工验证或错误页。
我在诊断工具中只保留页面状态和脱敏后的响应摘要:
import hashlib
import json
def response_summary(status, body):
raw = body.encode("utf-8", errors="replace")
return {
"status": status,
"length": len(raw),
"sha256": hashlib.sha256(raw).hexdigest(),
"looks_like_json": body.lstrip().startswith(("{", "[")),
"looks_like_html": "<html" in body[:500].lower(),
}
def write_diagnostic(path, record):
with open(path, "w", encoding="utf-8") as file:
json.dump(record, file, ensure_ascii=False, indent=2)
真实浏览器只用于本人有权限的测试,不把浏览器 Cookie 导出到脚本、数据库或文章。测试结束后关闭页面,清理临时用户目录。
六、第四步:为什么纯协议方式容易失效
纯协议实验需要补齐浏览器脚本依赖。项目中的 tianjin_rs_env.js 就是一个环境桩,提供了部分 window、document、navigator 和浏览器 API 的最小替代。
抽象结构如下:
global.window = global;
global.document = {
cookie: "",
createElement() {
return {};
}
};
global.navigator = {
userAgent: "local-test",
language: "zh-CN"
};
function get_cookie_names() {
return document.cookie
.split(";")
.map(item => item.trim().split("=")[0])
.filter(Boolean);
}
这个示例只展示本地环境桩和 Cookie 名称解析,不执行任何真实 RS 脚本,也不生成线上 Cookie。
纯协议方式的主要问题是:
- 页面脚本可能依赖没有补齐的 Web API。
- 不同脚本版本会访问不同对象。
- 属性值、异常行为和执行时序可能被检查。
- 页面脚本可能把路径、时间和环境一起参与计算。
- 服务器可能要求浏览器上下文中的连续状态。
所以我把纯协议代码定位为“离线协议研究工具”,不把它作为绕过线上访问控制的通用方案。
七、如何定位环境缺口
环境桩出现报错时,我先记录缺失的对象和调用位置:
function missing(name) {
throw new Error("missing browser capability: " + name);
}
global.DOMParser = function () {
missing("DOMParser");
};
global.XMLHttpRequest = function () {
missing("XMLHttpRequest");
};
Python 侧只保存错误类型和错误摘要:
def safe_error(exc):
text = str(exc).replace("\n", " ")
return {
"type": type(exc).__name__,
"message": text[:240],
}
我不会通过无限添加假的浏览器属性来“骗过”脚本。每补一个对象,都先确认它是为了本地离线解析,还是已经进入真实访问控制逻辑。后一种情况就停止纯协议实验,改用正常浏览器流程或向服务方申请测试接口。
八、Cookie 的生命周期
动态 Cookie 至少要记录四个维度:
生成时间
作用域
过期时间
对应页面或会话
我在本地用结构化记录保存 Cookie 元信息,不保存值:
def cookie_metadata(cookie):
return {
"name": cookie.get("name", ""),
"domain": cookie.get("domain", ""),
"path": cookie.get("path", ""),
"secure": bool(cookie.get("secure")),
"http_only": bool(cookie.get("httpOnly")),
"expires": cookie.get("expires"),
}
如果同名 Cookie 在不同路径或域名下出现,不能简单地用字典覆盖。调试时要保留完整的作用域信息;生产代码则使用浏览器或 HTTP 客户端自己的 Cookie Jar。
九、URL 参数和请求顺序
项目中普通客户端请求失败的一类原因,是 URL 形态不一致。为了排查,我会把 URL 拆成几个部分:
from urllib.parse import urlsplit, parse_qsl
def inspect_url(url):
parsed = urlsplit(url)
return {
"scheme": parsed.scheme,
"host": parsed.netloc,
"path": parsed.path,
"query_keys": [key for key, _ in parse_qsl(parsed.query)],
"fragment_present": bool(parsed.fragment),
}
我只比较:
- 路径是否相同;
- 查询参数是否缺少;
- 参数顺序是否发生变化;
- 是否多了一层 URL 编码;
- 请求是否发生在页面初始化完成之前。
如果服务器要求动态 URL 参数或浏览器上下文,文章中不复现具体参数生成过程,也不建议把抓到的参数硬编码。正确做法是使用官方接口、测试环境或浏览器正常流程。
十、响应分类
我把响应分为四类,而不是只看状态码:
def classify_response(status, body):
text = body.lstrip().lower()
if status in {401, 403, 429}:
return "access-control"
if text.startswith("<html") and "verify" in text[:2000]:
return "manual-verification"
if text.startswith(("{", "[")):
return "business-response"
if "<html" in text[:500]:
return "html-page"
return "unknown"
对应处理方式:
business-response → 进入业务 JSON 解析
html-page → 检查是否是跳转或初始化页
manual-verification → 停止任务,人工完成验证
access-control → 停止重试,检查权限和访问频率
unknown → 保存摘要,人工检查样例
遇到 403 或人工验证页时,我不会继续增加请求频率,也不会尝试伪造 Cookie。任务暂停后检查授权、接口文档、测试环境和访问频率。
十一、版本变化的对比方法
RS 脚本可能随站点版本变化。每次测试我会保存:
{
"captured_at": "2026-09-14T00:00:00Z",
"page_status": 200,
"html_sha256": "sample-only",
"inline_script_sha256": ["sample-only"],
"external_script_urls": ["https://example.invalid/app.js"],
"external_script_sha256": ["sample-only"],
"cookie_names": ["sample_cookie"],
"response_class": "html-page"
}
比较两个样例时,先看脚本摘要和 Cookie 名称是否变化,再看页面执行顺序。不要直接对比完整 Cookie 值,也不要把历史会话当作新会话使用。
十二、测试夹具
我会为 HTML 解析和响应分类准备本地夹具:
from pathlib import Path
def load_fixture(name):
return Path("fixtures", name).read_text(encoding="utf-8")
def test_bootstrap_fixture():
html = load_fixture("page_with_bootstrap.html")
report = analyze_bootstrap(
html,
"https://example.invalid/"
)
assert report["meta_count"] >= 1
assert report["external_scripts"]
def test_response_classification():
assert classify_response(200, '{"ok":true}') == "business-response"
assert classify_response(403, "blocked") == "access-control"
夹具里只放虚构的页面和虚构的脚本,不把线上 HTML、Cookie 或账号信息直接提交到仓库。这样既能测试解析逻辑,也不会让测试代码变成线上访问脚本。
十三、运行和排查顺序
我本地排查时按下面顺序执行:
- 确认 Python、Node.js 和页面分析依赖安装正常。
- 读取本地 HTML,输出 meta、脚本数量和摘要。
- 比较不同版本 HTML 和脚本摘要。
- 用真实浏览器确认页面是否能正常初始化。
- 只记录 Cookie 元信息和响应分类。
- 对协议实验只做本地环境桩测试。
- 遇到人工验证、权限错误或频率限制时停止任务。
python3 -m py_compile 3_rs_request.py 3_rs_protocol.py
node --check tianjin_rs_env.js
如果浏览器能打开而普通客户端失败,先判断失败发生在页面初始化、Cookie 建立、URL 处理还是权限层,不要把所有问题都归结为“少了请求头”。
十四、公开文章中的脱敏规则
我在发布教程前会检查以下内容:
完整 Cookie → 删除
Authorization / Token → 删除
AppKey / 私钥 → 删除
真实域名和业务路径 → 替换
真实税号和账号 → 替换
可直接生成风控 Cookie 的代码 → 删除
外部反爬服务地址 → 删除
保留的内容包括:
- HTML 结构解析;
- 脚本依赖分析;
- 浏览器和纯协议两种路线的差异;
- Cookie 生命周期记录方式;
- 响应分类和错误处理;
- 本地测试夹具;
- 脱敏日志和版本对比。
总结
这个项目最终让我把天津 RS 流程归纳成了三条线:
页面线:HTML → meta / inline script / external script
环境线:window / document / navigator / Cookie / 时序
请求线:最终 URL → 会话状态 → 响应分类
浏览器路线适合确认页面真实行为,纯协议路线适合做离线代码分析和环境依赖梳理。两者都不能替代授权,动态 Cookie 也不能当成永久凭据。
真正排查时,我先保存脱敏样例,再定位脚本入口、环境依赖和请求边界,最后用本地夹具验证。遇到访问控制或人工验证时,按正常流程处理,不把协议分析代码用于绕过平台安全策略。
十五、小白从零开始:先跑离线 Demo
如果第一次接触这类项目,不要一开始就运行线上请求脚本。先把本地结构分析 Demo 跑通,确认 Python、HTML 解析和摘要计算都正常。
进入项目目录:
cd "/Users/liuweifeng/bnballobj/web_nixiang/天津RS协议"
python3 --version
python3 beginner_rs_demo.py
这个 Demo 不需要安装浏览器,也不访问网络。正常输出会包含:
HTML 分析结果:
{'meta_count': 1, 'metas': [...], 'inline_scripts': [...],
'external_scripts': ['/static/app.demo.js']}
响应分类:
200 business-response
200 html-page
403 access-control
我建议按下面的顺序理解输出:
meta_count表示页面里找到了多少个可能参与初始化的 meta。inline_scripts表示页面内联脚本的长度和 SHA-256 摘要。external_scripts表示页面引用了哪些外部脚本路径。business-response表示本地样例看起来像业务 JSON。access-control表示程序应该停止,而不是继续重试。
这个 Demo 对应原项目中的“HTML 解析、脚本盘点和响应分类”部分。它没有读取 Cookie,也没有生成 RS 参数,所以可以在没有账号、没有浏览器、没有线上环境的情况下复现。
如果命令提示找不到 Python,先安装 Python 3.10 或更高版本;如果输出是乱码,确认终端和源文件都使用 UTF-8。Windows 用户把 cd 后面的路径改成自己的项目目录即可。
十六、从 Demo 过渡到项目代码
本地 Demo 跑通后,再阅读原项目:
beginner_rs_demo.py → 理解 HTML 和响应分类
3_rs_request.py → 理解浏览器路线的类和方法
3_rs_protocol.py → 理解协议分析路线的步骤
tianjin_rs_env.js → 理解环境桩为什么存在
我阅读 3_rs_protocol.py 时会把每一步对应到下面的表格:
请求页面 → 获取 HTML 样例
读取 meta 和 script → 建立脚本依赖清单
执行环境桩 → 观察缺失的浏览器对象
更新会话状态 → 只记录元信息
调用业务接口 → 仅在授权测试环境进行
在线部分不能用文章中的命令直接复现,因为它依赖实时页面、实时会话和平台安全策略。能完整复现的是本地解析、摘要和错误分类;需要在线联调时,使用正常浏览器、测试账号和平台允许的接口。
评论 0