天津 RS 协议分析:页面初始化、动态 Cookie 与浏览器环境排查

张 张站长 · 2026-09-14 11:53 · 技术-逆向 · 49 次浏览 · 0 条评论

补充小白从零开始的离线 Demo、运行命令、预期输出和从 Demo 过渡到项目代码的完整说明。

天津 RS 协议分析:页面初始化、动态 Cookie 与浏览器环境排查

这篇文章记录我整理天津电子税务页面 RS(瑞数)防护流程的过程。项目最初是为了理解:为什么同一个页面在浏览器中可以正常打开,而直接使用普通 HTTP 客户端时会返回拦截页、跳转页或空响应。

我把问题拆成了四层:

页面初始化
    ↓
服务端下发动态参数和脚本
    ↓
浏览器执行脚本并建立会话状态
    ↓
后续请求携带会话状态和正确的 URL 形态

RS 在这里不是单一的“加密算法”。它更接近一套由页面脚本、动态 Cookie、请求时序、浏览器环境和 URL 处理共同组成的访问控制机制。项目中既有浏览器自动化方案,也有使用 requests + execjs 分析页面脚本的纯协议实验。

本文只针对本人或所在组织已经获准测试的环境。文章里的域名、Cookie、路径和参数均为抽象示例,不包含真实访问凭据、设备指纹、动态 Cookie 生成结果或可直接复用的风控绕过流程。

RS 分析总览

一、项目文件

项目主要由三个文件组成:

天津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。

纯协议方式的主要问题是:

  1. 页面脚本可能依赖没有补齐的 Web API。
  2. 不同脚本版本会访问不同对象。
  3. 属性值、异常行为和执行时序可能被检查。
  4. 页面脚本可能把路径、时间和环境一起参与计算。
  5. 服务器可能要求浏览器上下文中的连续状态。

所以我把纯协议代码定位为“离线协议研究工具”,不把它作为绕过线上访问控制的通用方案。

七、如何定位环境缺口

环境桩出现报错时,我先记录缺失的对象和调用位置:

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 或账号信息直接提交到仓库。这样既能测试解析逻辑,也不会让测试代码变成线上访问脚本。

十三、运行和排查顺序

我本地排查时按下面顺序执行:

  1. 确认 Python、Node.js 和页面分析依赖安装正常。
  2. 读取本地 HTML,输出 meta、脚本数量和摘要。
  3. 比较不同版本 HTML 和脚本摘要。
  4. 用真实浏览器确认页面是否能正常初始化。
  5. 只记录 Cookie 元信息和响应分类。
  6. 对协议实验只做本地环境桩测试。
  7. 遇到人工验证、权限错误或频率限制时停止任务。
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

我建议按下面的顺序理解输出:

  1. meta_count 表示页面里找到了多少个可能参与初始化的 meta。
  2. inline_scripts 表示页面内联脚本的长度和 SHA-256 摘要。
  3. external_scripts 表示页面引用了哪些外部脚本路径。
  4. business-response 表示本地样例看起来像业务 JSON。
  5. 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

还没有评论
← 全部文章

找站长

站长微信二维码 扫码加微信
售后群二维码 扫码进售后群
站长收款码 建站不易,感谢打赏
微信 18581469390
本站不支持自助充值;需要充值或开通额度,请直接联系站长。
加微信可以找我重置密码、充值、问工具怎么用