逆向分析:中国干部网络学院浦东分院(CELA)课程进度提交接口

〇、结论先行

中国干部网络学院浦东分院(cela.e-celap.cn)的课程学习进度提交接口不做连续性校验——一次 pulseSaveRecord 请求即可将任意课程从 0% 刷到 100%。唯一需要注意的是 watchPoint 参数必须用 HH:MM:SS 格式字符串,而不是秒数。

⚠️ 声明:本文仅做技术逆向分析记录,不提供成品脚本,不代刷,不对他人账号做任何操作。请合规完成个人学习任务。

一、平台技术架构

与此前做过的 HEBGB(河北干部网络学院)完全不同,CELA 是一个现代化 SPA 应用:

对比项 HEBGB(河北) CELA(浦东)
前端框架 传统 JSP 服务端渲染 Vue 2 + Element UI(DSF 五代框架)
视频托管 站点自有 上海广播电视台 zpyapi.shsets.com
视频格式 普通 MP4 HLS(m3u8)+ 动态签名鉴权
登录加密 AES-ECB(前端 CryptoJS) JWT Token(HS256 签名)
防刷课 无 有开关,但当前未开启

二、侦察过程

2.1 第一步:抓加载链路

用 Playwright(headless: true)注入 Cookie 加载课程播放页,通过 page.on('request') / page.on('response') 拦截全部 XHR/Fetch 请求。

页面加载后的请求序列:

📋 页面加载请求链路
1. GET /inc/meta/init                     # 元数据初始化
2. GET /dsfa/nc/cela/api/pushCourse         # 防刷课检查(返回"防刷课开关未开启")
3. GET /inc/nc/course/play/getPlayBase      # 课程基础信息(标题、教师、封面)
4. GET /inc/nc/course/play/getPlayTrend    # 课程进度树(课件列表 + finishedRate)
5. GET /inc/nc/course/play/getPlayRecord   # 单个课件播放记录(进度 + 上次观看位置)
6. GET /nc/celapcourse/getRoomId           # 视频文件 ID
7. GET zpyapi.shsets.com/player/get       # 视频 HLS 播放地址
8. POST zpyapi.shsets.com/player/signature # 签名验证

页面加载完成后,进度上报接口 pulseSaveRecord 不会自动触发——它只在视频播放过程中由播放器心跳定时器调用。Headless 模式下 Chromium 不会自动播放视频,所以拿不到这个请求。

2.2 第二步:从 JS 源码挖进度提交逻辑

下载站点核心 JS 文件 dsf-nc.pc.js(约 2MB),直接搜索关键字 pulseSaveRecord:

🔍 关键代码定位
// 组件 props 中定义的提交 URL
saveSourceUrl: { default: "nc/course/play/pulseSaveRecord" }

// 心跳定时器回调函数
function n() {
  e.saveTimer = setTimeout(function() {
    var i = {
      courseId: e.id,              // 课程 ID
      coursewareId: e.curPlayItem.id, // 课件 ID
      watchPoint: ...,             // ← 关键参数
      pulseTime: e.saveTime,       // 心跳间隔(默认 10 秒)
      pulseRate: ...               // 播放倍率
    };
    // 提交方式:jQuery $.ajax (form-urlencoded)
    dsf.http.post(saveSourceUrl, i, $$webRoot.nc);
  }, i);
}

// 页面关闭时的兜底提交(sendBeacon)
navigator.sendBeacon("nc/course/play/pulseSaveRecord", formData);

找到了完整的参数结构,但还缺一个关键信息:watchPoint 的值到底是什么格式?

2.3 第三步:拦截真实播放器心跳

换成 headless: false 模式,手动触发视频播放,等待心跳自动触发后拦截真实请求:

📦 真实心跳请求(拦截结果)
POST /inc/nc/course/play/pulseSaveRecord
Content-Type: application/x-www-form-urlencoded;charset=UTF-8

courseId=e1869a465dbb44cc803e402786f0bc61
&coursewareId=438710feb4cd4b85b0630de9344d76b7
&watchPoint=00%3A12%3A19          ← 注意!是 HH:MM:SS 格式(URL 编码后的冒号)
&pulseTime=10
&pulseRate=1

/* 响应 */
{
  "success": true,
  "state": 20000,
  "message": "成功",
  "data": {
    "finishedRate": 31,           // 更新后的进度
    "coursewareIsFinished": false
  }
}

真相大白:watchPoint 不是秒数,而是 HH:MM:SS 格式的时间字符串(如 00:12:19)。之前传秒数导致后端解析失败,返回 state:20002 message:"1"(参数校验错误)。

三、关键发现:无连续性校验

知道格式后,直接构造一个接近视频末尾的 watchPoint(视频时长 2578 秒 ≈ 00:42:58),一发 POST:

⚡ 一次性完成
// 构造末尾 watchPoint
POST /inc/nc/course/play/pulseSaveRecord
courseId=e1869a465dbb44cc803e402786f0bc61
&coursewareId=438710feb4cd4b85b0630de9344d76b7
&watchPoint=00:42:52       ← 直接发视频末尾
&pulseTime=10
&pulseRate=1

/* 响应 —— 直接 100% */
{
  "success": true,
  "data": {
    "finishedRate": 100,
    "coursewareIsFinished": true
  }
}

服务端完全不做播放位置连续性校验——从 00:12:17(31%)直接跳到 00:42:52(100%),一次请求完成。

四、完整接口文档

4.1 接口路由前缀

站点的 $$webRoot 配置决定各模块的 URL 前缀:

模块 路由前缀 说明
nc /inc/ 网络学院(课程、播放记录等)
dsfa / 平台基础
teas / 教务模块
exam /exam/ 考试系统

4.2 核心接口清单

接口 方法 参数
/inc/nc/course/play/getPlayBase GET courseId
/inc/nc/course/play/getPlayTrend GET courseId
/inc/nc/course/play/getPlayRecord GET coursewareId, courseId
/inc/nc/course/play/pulseSaveRecord POST courseId, coursewareId, watchPoint, pulseTime, pulseRate
/nc/celapcourse/getRoomId GET kjid(课件 ID)

4.3 进度提交详解

POST /inc/nc/course/play/pulseSaveRecord
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
X-Requested-With: XMLHttpRequest

# 参数
courseId     = "e1869a465dbb..."    # 课程 UUID
coursewareId = "438710feb4cd..."    # 课件 UUID
watchPoint   = "00:42:52"           # ⚠️ HH:MM:SS 格式字符串
pulseTime    = "10"                # 心跳间隔(秒)
pulseRate    = "1"                 # 播放倍率

# 成功响应
{ "data": { "finishedRate": 100, "coursewareIsFinished": true },
  "success": true, "state": 20000, "message": "成功" }

# 参数错误(watchPoint 传了秒数而非 HH:MM:SS)
{ "data": {}, "success": false, "state": 20002, "message": "1" }
踩坑总结:
1. watchPoint 必须是 HH:MM:SS 字符串(如 00:42:52),传数字会返回 state:20002
2. 路由前缀是 /inc/($$webRoot.nc = "/inc/"),不是 /nc/
3. Content-Type 必须带 charset=UTF-8,否则偶发 20001

五、防刷课机制分析

站点有一个 pushCourse 防刷课入口,加载课程页时会自动调用:

GET /dsfa/nc/cela/api/pushCourse
GET /dsfa/nc/cela/api/pushCourse?user_id=xxx&course_id=xxx

/* 当前响应 */
{ "data": {}, "success": true, "state": 20000,
  "message": "防刷课开关未开启!" }

防刷课开关由站点全局配置 window.$$serverConfig 中的 setting_nc_period_process_rate 等参数控制。当前值为 90(进度阈值),但 pushCourse 接口明确返回「未开启」。

播放器配置中还有几个相关开关:

配置项 当前值 含义
setting_nc_player_is_drag 0 进度未达 100% 时禁止拖拽进度条
setting_nc_player_is_speed 1 允许倍速播放
setting_nc_period_process_rate 90 学时认定进度阈值(%)

六、与 HEBGB 平台的对比总结

两个干部网络学院平台的技术栈差异很大,但自动化学习的突破口殊途同归——进度上报接口不校验播放位置连续性:

维度 HEBGB CELA
进度提交接口 seekNew.do pulseSaveRecord
时间参数格式 秒数(整数) HH:MM:SS 字符串
连续性校验 无 无
防刷课 无 有开关,当前未开启
认证方式 Session Cookie + 图片验证码 JWT Token Cookie
逆向难度 中(AES 加密 + 验证码识别) 低(Token 已有,只需抓接口格式)

七、方法论总结

对于这类「视频学习平台」的进度自动化,通用侦察方法论:

① 抓加载链路 用 Playwright headless 模式加载页面,拦截全部 XHR/Fetch 请求,找出课程信息、播放记录等 GET 接口。进度提交接口通常不会在页面加载时自动调用。

② 搜 JS 源码 下载前端核心 JS 文件,搜索 saveProgress / pulse / heartbeat / record 等关键字,定位进度提交的 URL 和参数结构。

③ 拦真实心跳 如果 JS 混淆严重或参数值不明确,切换 headless: false,手动播放视频触发心跳,拦截真实请求获取准确的参数格式。这是最关键的一步——CELA 的 HH:MM:SS 格式就是靠这一步发现的。

④ 测连续性 构造一个跳到接近末尾的参数值,测试服务端是否允许非连续跳转。如果允许 → 一次请求完成;如果不允许 → 需要模拟逐步递增的心跳序列。

⑤ 查防刷配置 检查站点全局配置(通常在 window.$$serverConfig 或类似全局变量中),确认防刷课、进度校验等开关是否开启。

再次声明:以上内容仅做技术分析记录。请在自己账号下合规完成学习任务,不要用于代刷或商业化。学习平台的接口随时可能更新(如开启防刷课、增加连续性校验),届时本文方法将失效。
© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容