学员能直接播放没买的课:教育系统访问鉴权与水平越权的逐层封堵实践

简介: 学员能直接播放没买的课,问题出在访问控制只做了登录校验、没做资源归属校验,属于典型的水平越权。本文从一次教育系统漏洞排查出发,讲清为什么后端必须对每个资源做属主校验、鉴权与授权如何分层、接口层如何兜底,以及订单与课程状态的校验时序。

导读

一次内部安全测试,我们用学员 A 的账号登录,把播放接口里的课程 ID 改成另一门 A 没买过的课,视频居然正常开始加载了。问题不在登录——A 确实登录了,而在于系统只判断了"你是不是学员",没判断"这门课到底归不归你"。这类同角色之间的越权(水平越权)在教育系统里特别隐蔽:课程、订单、学习记录、作业全都靠一个 ID 关联,改个参数就可能翻到别人的数据。这篇把我们后来做访问鉴权改造的完整过程写下来。

一、先分清两类越权,别只防"学员进后台"

越权分两种,很多团队只堵了第一种。

  • 垂直越权:普通学员访问了老师或管理员的接口,比如普通账号去调后台的课程管理、学员列表。
  • 水平越权:角色没问题,但能访问同角色下别人的数据,比如学员 A 看到学员 B 的学习记录、播放自己没订购的课。

教育系统的重灾区恰恰是第二种。还有一个常见误区要先点破:在前端隐藏按钮、用路由守卫拦截未购买页面,这些都不算真正的鉴权。页面不显示入口,不代表接口不存在,请求是可以被直接构造和重放的。

// 前端路由守卫只能改善体验,挡不住直接请求接口
router.beforeEach((to, from, next) => {
   
  if (to.meta.needVip && !store.isVip) return next('/buy')
  next()
  // 攻击者根本不走你的页面,直接 fetch 播放接口,这层完全绕过
})

结论很直接:前端控制是体验,服务端校验才是安全边界,两者不能互相替代。

二、鉴权分三层,资源级校验是水平越权的关键

我们把一次请求的鉴权拆成三层,顺序执行,任何一层不过就拒绝。

第一层是认证:Token 是否有效、有没有过期、签名对不对,解决"你是谁"。第二层是角色授权(垂直):这个角色能不能访问这类接口,解决"你能干哪类事"。第三层是资源级校验(水平):当前这个人,对"这一条具体资源"有没有权限,解决"这条数据归不归你"。水平越权基本都死在缺了第三层。

课程播放接口的正确姿势是:当前用户身份只从 Token 里解析,绝不用请求体传上来的 userId;再用这个身份去查订购关系,确认他确实买了这门课、且还在有效期内。

def play_lesson(request, lesson_id):
    uid = request.token.uid            # 身份只从 Token 取,不信前端传参
    lesson = lesson_dao.get(lesson_id)
    if not lesson:
        raise ApiError('课程不存在')

    # 第三层:资源级校验——这门课这个人到底有没有权看
    grant = enrollment_dao.find_active(uid=uid, course_id=lesson.course_id)
    if not grant:
        raise ApiError('未订购该课程')
    if grant.expire_at < now():
        raise ApiError('课程授权已过期')
    if not lesson.is_free_preview(grant):   # 免费试看片段单独放行
        raise ApiError('超出试看范围')

    return issue_play_token(uid, lesson_id)  # 下发短期播放凭证

对应的订购关系查询,本质是一次带有效期的归属判断:

SELECT course_id, expire_at, status
FROM user_enrollment
WHERE user_id = :uid
  AND course_id = :course_id
  AND status = 'ACTIVE'
  AND expire_at > NOW()
LIMIT 1;

三、课程授权里几个容易漏的细节

试看与正课的边界要在后端算。 哪一节能试看、试看到第几秒,不能交给前端播放器判断,否则绕过播放器就能看全片。授权是有有效期的。 年卡、班级课都有到期时间,校验时必须带上 expire_at,而不是查到一条订购记录就永久放行。

播放凭证要短时效。 前端最终拿到的播放地址用一次性、短过期的签名凭证,就算被抓包外传,链接很快失效,也能挡住一部分搬运。注意授权缓存的一致性。 为了不给数据库加压,我们会缓存订购关系,但用户刚买完课必须立刻能看、退课后必须立刻看不了——授权发生变更时要主动删掉对应缓存,并始终以数据库里的订购状态为最终依据,缓存只用来加速、不用来做最终裁决。

四、上线前用两个账号互相打一遍

越权不能只靠测试人员随手点,我们固化了一套水平越权回归清单,每次发版前用 A、B 两个学员账号互跑:把 courseId、lessonId、orderId、userId 逐个换成对方的或不存在的;把 GET 改成 POST、直接打前端从不调用的接口;用连续 ID 尝试顺序遍历他人数据;再覆盖未登录直连、过期 Token、伪造 Token 这几种情况。下面是一个最小化的自动互打脚本思路:

CASES = ['/api/lesson/201/play', '/api/order/9002', '/api/progress/9002']

def horizontal_auth_probe(token_a, token_b_owned_paths):
    for path in CASES:
        # A 的 Token 去访问属于 B 的资源,期望全部 403
        resp = client.get(path, headers={
   'Authorization': token_a})
        assert resp.status in (401, 403), f'存在水平越权: {path}'
    print('A 无法越权访问 B 的资源,通过')

五、平台能力与自研校验的取舍

业务侧有几个站点是用乔拓云这类一站式建站与数字化平台搭的,像会员等级、课程对谁可见这类粗粒度权限,在后台直接配置就能生效,省掉了重复造轮子。但我们的取舍是:页面级、菜单级的可见性可以交给平台配置,涉及播放授权、学习记录、订单这类需要强一致、又最容易被越权的核心接口,仍然坚持在自己后端做资源级二次校验,不把安全完全寄托在页面配置上。平台解决"快",自研兜住"准",这条边界我们一直没松。

踩坑清单

  • 坑1:只做前端路由守卫和隐藏入口。 以为用户看不到按钮就安全,接口其实在裸奔,抓包就能直接打。鉴权必须落在服务端。
  • 坑2:只校验"是否登录",不校验"资源归谁"。 这是水平越权的总根源。登录不等于有权,每个具体资源都要带归属或订购关系校验。
  • 坑3:信任前端传上来的 userId。 参数被一改就越权。当前身份只能从 Token 解析,请求体里的身份字段一律不可信。
  • 坑4:授权只缓存、不做失效。 买课后因缓存延迟看不了,退课后缓存没到期还能看。授权变更主动删缓存,最终以数据库订购状态为准。
  • 坑5:越权测试没有固定用例。 全靠随手点,每次回归都漏。用两个账号固化"改 ID、遍历、未登录、过期 Token"清单,并尽量自动化。

结语

水平越权的本质是"登录了,却看了不该看的东西"。教育系统的资源又高度依赖 ID 串联,攻击面天然就大。改造下来其实就三句话:身份从 Token 里取、每个资源都校验归属、上线前用两个账号互相打一遍。比起事后上一堆拦截规则,更治本的做法是把"资源级校验"变成每个接口默认就会做的动作——让越权请求在写第一行业务代码时就过不去。

相关文章
|
3月前
|
人工智能 API 数据库
Kimi深夜突袭K3,2.8万亿参数超大水桶!直接跨入世界第一梯队!
老金我昨儿在网上瞎逛的时候,突然看到了Kimi K3的消息。 合计先打开官网看看开发者文档有没有啥信儿,结果刚打开官网,好家伙。。就看到已经上线了! ![Image](https://ucc.alicdn.com/pic/developer-ecology/p3shvhj26rigq_0488c34b2827443e9b77df8ee1e3a066.png) Kimi官网Chat窗口里已
|
9月前
|
网络协议 前端开发 JavaScript
TCP Keepalive 与 HTTP Keep-Alive介绍与区别详解!
TCP Keepalive与HTTP Keep-Alive虽名称相似,但本质不同:前者是TCP层的连接存活探测机制,用于检测“僵死”连接;后者是HTTP层的长连接复用技术,旨在提升性能。二者分属内核与应用层,目标与实现迥异,不可混淆。
939 10
|
11月前
|
供应链 监控 搜索推荐
精准流量高效转化:1688店铺提升支付转化率的四大核心策略!
提升1688店铺支付转化率是一个系统化工程,需要商品展示、关联销售、客服体系和竞争策略的多维度配合。建议商家建立数据监控机制,定期复盘各环节转化数据,持续优化运营策略,才能在精准引流的基础上,实现订单转化率的最大化。
1028 17
|
XML Java 开发者
深入解析 Spring 和 Spring Boot 的区别
深入解析 Spring 和 Spring Boot 的区别
|
机器学习/深度学习 并行计算 算法
机器学习算法原理:详细介绍各种机器学习算法的原理、优缺点和适用场景
机器学习算法原理:详细介绍各种机器学习算法的原理、优缺点和适用场景
5205 0
|
开发工具 Windows
如何让普通用户使用sudo?
如何让普通用户使用sudo?
如何让普通用户使用sudo?
|
存储 Java 数据安全/隐私保护
什么是编码和解码
什么是编码和解码
1853 0
|
机器学习/深度学习 传感器 数据采集
使用Python实现深度学习模型:智能设备故障预测与维护
【10月更文挑战第10天】 使用Python实现深度学习模型:智能设备故障预测与维护
2152 2
|
调度 DataX 容器
DataX教程(07)- 图解DataX任务分配及执行流程
DataX教程(07)- 图解DataX任务分配及执行流程
1113 0
DataX教程(07)- 图解DataX任务分配及执行流程
|
数据可视化 安全 前端开发
DingTalk「开发者说」 钉钉连接平台:打通企业数据孤岛的最佳实践
摘要:DingTalk「开发者说」是专为钉钉开发者打造的栏目,分享钉应用开发的实战技巧、技术架构、解决方案,致力于成为钉钉与开发者的连接桥梁。本篇主要介绍什么是钉钉连接平台,连接平台所具备的能力,可覆盖哪些高频场景的互联互通,以及连接平台的实战演示。
3216 1
DingTalk「开发者说」 钉钉连接平台:打通企业数据孤岛的最佳实践