教务管理平台批量数据维护实战:CDP 抓包、接口逆向与自动化脚本

场景:教务管理平台(uManage)大规模数据维护
目标:跨年级批量增删学生、班级、教师账号与任课配置
手段:Chrome DevTools Protocol + 前端接口逆向 + Node.js 批量脚本
规模:一次会话内完成 5000+ 学生、70+ 班级、50+ 教师账号的增删改
环境:Windows / Chrome 153 / Node.js 22

教务系统这类内部平台,通常只提供”一个个点”的表格 UI。当需求变成”把整个高二年级的学生清空重建””按名单批量建班导学生””按表格批量设置任课教师”时,纯手工操作是不现实的。这篇文章记录一次完整实战:如何让浏览器被程序驱动、如何把 UI 操作还原成接口、以及踩到的每一个坑。


第一关:让浏览器能被程序驱动

技术路线很直白:用 Chrome 的远程调试协议(CDP)接管一个真实浏览器实例,复用它的登录态,在页面上下文里直接发 fetch。难点不在 CDP 本身,而在于让这个浏览器真的能起来、并且活下来。

Chrome 153 的 App-Bound Encryption

Chrome 从 127 版本起引入了 App-Bound Encryption(ABE),Cookie 等敏感数据的加密密钥会绑定到原始 profile 路径。带来的直接后果是:把默认 profile 复制一份到别的目录再启动,登录态是解不开的。

同时,新版本对默认 profile 开启了保护——直接对日常使用的 profile 加 --remote-debugging-port,端口根本不会监听。

注意
这也意味着”复制 profile 迁移登录态”这条路彻底走不通。Cookie 解不开,只能换思路:不迁移登录态,改为自己注入 Cookie。

进程被 job object 回收

第二个坑更隐蔽:从命令行启动的 Chrome,会在命令返回后被操作系统连同进程组一起回收——端口刚监听上就没了。

依次排除过的方案:

  • nohup ... & + disown:无效,Windows 上 detached 语义和 POSIX 不同
  • cmd /c start:进程仍在同一个 job 里
  • 计划任务 / WMIC:被安全策略直接拉黑
  • explorer.exe 转发启动 bat:本环境不执行 bat
  • Shell.Application COM:被安全策略判定为可执行任意代码而拦截

最终稳定可用的方式,是用一个常驻后台任务承载浏览器进程:把 Chrome 的启动放进一个长期运行的后台任务里,进程便不会再随单次调用消失,后续所有脚本都能连上同一个 127.0.0.1:9222。

小贴士
判断调试实例是否存活,最直接的办法是 curl http://127.0.0.1:9222/json/version。返回 JSON 即正常,返回空说明实例已经没了。

实例就绪后,注入登录态并打开目标页只需要三步:

// 连上浏览器
const { webSocketDebuggerUrl } = await (await fetch('http://127.0.0.1:9222/json/version')).json();
const ws = new WebSocket(webSocketDebuggerUrl);

// 1) 全域注入 Cookie(绕开 ABE 无法迁移登录态的问题)
await send('Storage.setCookies', { cookies: [{
  name: 'SESSION', value: '<登录态>',
  domain: '.example.com', path: '/', secure: true, sameSite: 'Lax',
}]});

// 2) 开一个新标签并附着调试会话
const t = await send('Target.createTarget', { url: 'about:blank' });
const { sessionId } = await send('Target.attachToTarget', { targetId: t.result.targetId, flatten: true });

// 3) 导航,之后所有 fetch 都自动带上登录态
await send('Page.navigate', { url: TARGET_URL }, sessionId);

第二关:把 UI 操作还原成接口

浏览器能被驱动之后,剩下的就是”翻译”工作:把所有要批量执行的操作,抓出它背后的 HTTP 请求。

做法是开启 CDP 的 Network 域监听,人工在页面上完整操作一遍(删一个学生、建一个班、加一位教师),把过程中的请求全部落盘,然后分析参数结构。

因为页面本身是前端框架渲染的单页应用,接口规律非常整齐:统一的 POST/GET + 表单或 JSON 体 + CSRF token 放在 URL query 上。

核心接口清单

操作 方法与路径 关键参数
删除学生 POST /student/delete studentListJson(含 studentId / name / classId)
批量新增学生 POST /student/batch/add studentJsonStr、password(RSA 密文)
查询学生列表 GET /student/list/new studentQueryDTOStr,limit 上限 50
批量删除班级 POST /class/class-batch-delete classIdAndTypes,需 X-Requested-With
班级课程教师 GET /class/course-teacher-list classId / teachingCycleId
批量设置任课 POST /teaching/batch-org-teaching-set-with-lock-status JSON 体 + token 放 query
新增教师 POST /teacher/add-teacher 表单字段见下
删除教师 POST /teacher/delete-teacher ids / schoolId / dimissionReason
教师名册 GET /teacher/list/new userLoginName / page / limit
班级树与人数 GET /tree/class-tree gradeCode / teachingCycleId

接口逆向的六个坑

1. 密码字段是 RSA 密文

前端在提交前会调用 window.encryptedString(明文) 做 RSA-2048 加密,输出 512 位十六进制。一开始以为每名学生都要单独算一次密文,后来发现这是确定性加密——同样的明文永远得到同样的密文。

小贴士
既然密文可复用,批量新增时全部学生共用一个预先算好的密码密文即可,无需在脚本里实现 RSA。

2. limit 超过 50 会静默返回 HTML

列表接口传 limit=200 不会报错,而是返回一个 HTML 页面(登录/错误页),解析 JSON 就直接炸。表现为”某个班级复验结果变成 -1″。

修正:limit=50 固定,靠翻页拿全量。

3. 少一个请求头就返回 HTML

/class/class-batch-delete 缺少 X-Requested-With: XMLHttpRequest 时同样返回 HTML。凡是”接口返回 HTML”的情况,先怀疑请求头。

4. token 放错位置

批量设置任课的接口,CSRF token 必须放在 URL query 上,写进表单体会被拒绝。

5. 字段不全导致”假成功”

/teacher/add-teacher 如果只提交必填的姓名、密码,接口会返回“新增成功”,但数据库里根本没有这条记录。

逐个字段二分定位后确认:必须同时提交 idCardNo、teacherStaffNo、mobile、email、departmentListStr 五个空值字段,才会真正落库。

注意
这类”接口说成功、实际没写库”是最危险的坑——批量脚本跑完显示 100% 成功,实际数据一条没进。任何批量写操作之后,都必须独立复验。

6. 列表缓存延迟与翻页漏记录

教师列表接口有缓存:删除账号后立刻查询,结果还是旧的,大约半分钟后才更新。此外翻页抓取会漏记录(曾漏掉某个教师名下的第三个账号)。

修正:复验时按姓名精确搜索,而不是全量翻页。


第三关:怎么确认数据真的落库了

前端页面上的红底/黄底提示,其实是页面打开那一刻的本地校验快照,不会实时刷新——补完账号再回看,标记不会变,必须重开页面。

真正可靠的验证方式是反向核对:用读取类接口(如班级课程教师列表)把服务端真实数据拉下来,与提交前的表格逐格比对。

这次就靠这个方法抓出一个漏网之鱼:高二 36 个班、254 个教师填报格子,服务端比对显示 247 格完全一致,唯独 38 班整行 7 格没落库——提交时那一行被漏掉了,从页面上完全看不出来。


可复用的”技能包”

执行范式

整套流程沉淀成一个固定的五步范式:

  1. 读取:用接口拉服务端真实数据,建立基线
  2. 计划:本地算出保留/删除/新增清单,先跑 dry-run 打印计划
  3. 执行:逐条调用接口,status=1 才算成功,记录每一条响应
  4. 复验:独立再读一次服务端数据,逐条核对
  5. 备份:删除前导出完整清单(含 ID、账号、手机号),随时可追溯

脚本清单

脚本 职责
visit.mjs CDP 注入 Cookie + 打开目标页 + 截图
capture_net.mjs CDP 监听网络,抓取页面操作的真实请求
batch_delete.mjs 批量删除(进班删除学生 / 批量删班)
batch_add.mjs 按名单批量导入学生(复用密码密文)
extract_grid.mjs 提取页面表格网格与单元格底色标记
dump_roster.mjs 全量/按姓名拉取教师名册
delete_dup.mjs 重名账号去重(保留唯一)
compare.mjs 表格计划 vs 服务端实际,输出差异清单

重名账号的处理策略

批量设置教师时,最麻烦的是同名教师——系统提示”存在同名用户,请从下拉框中选择”,无法自动匹配。处理策略是让每个姓名在教师库里唯一:

  • 优先保留已登记手机号的账号
  • 同组都无手机号时,保留创建较早(ID 较小)的那一个
  • 删除前后各留一份清单,注明保留依据

写在最后

这类”内部系统自动化”的难度分布很有意思:CDP、接口逆向都是成熟技术,真正的成本几乎全花在环境对抗(浏览器加密策略、进程回收、安全拦截)和数据可靠性验证上。

三条经验值得记住:

  • 凡是接口,先抓包;UI 上点一次,胜过猜十次
  • 凡是批量写,必独立复验;”接口返回成功”和”数据真的进库”是两件事
  • 凡是批量删,先备份清单;可追溯比快更重要
提示
本文仅记录技术过程。所有批量操作均在获得系统管理员授权的前提下进行,文中涉及的平台名称、账号、姓名等敏感信息已做脱敏处理。请勿将相关技术用于未授权的系统。
© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容