登录与权限:会话、角色和操作审计

课前先保留判断:课前预习;课后用你的结果复验:课后复习。

今天从哪里开始

订单接口已经能查、能改,也有商品缓存。现在运营提出:只读员工能看订单,但不能取消;客服可以处理订单,却不能随意改商品价格。今天用这些具体动作来定义权限。

先区分客户与操作员工,再验证密码、建立服务端会话,最后在每个受保护入口检查角色。每次拒绝都要查询订单状态,确认后端真的没有执行。

能力与项目增量

今天要学会什么

应用已学函数/集合/装饰器/异常/上下文清理/async;认证、授权、Session、Cookie、CSRF 和审计。新增能力都落到:受保护订单 API、角色矩阵及拒绝后状态记录。

  • 认证与授权
  • 密码哈希和 HttpOnly Cookie
  • Session、过期与注销
  • RBAC、CSRF 和审计

课堂结束时,用自己的项目检查:

  • 能做出来:只读账号直接请求取消订单,验证 403 和状态不变,并展示正常输入的结果。
  • 能查出问题:针对“前端隐藏按钮冒充授权、把 Customer 当员工、会话故障时放行”中的风险,说明输入、预期与实际差异;失败后核对数据没有被误改。
  • 能解释修改:提交代码、测试和修改前后结果,讲清自己接受或拒绝了哪一处 AI 建议。

从已有 Python 语法读懂权限调用链

我先不增加新语法,而把前三周已学的规则用到真实请求:请求进入 → 找会话 → 判断角色 → 执行业务 → 提交审计 → 返回结果。

已学知识这次为什么需要商城实际位置与练习
字典、集合与条件分支不同角色允许的动作不同,未知动作应拒绝日包 may_write(role, action);补 viewer/support/admin 与未知值测试
函数、闭包与装饰器路由注册要记住应用与处理函数识读 @router.post、@app.middleware;注册机制不等于自动完成授权
自定义异常与捕获未登录、无权限、身份服务故障不能混成成功识读 AuthFailure,定位 401/403/503 的转换位置
async/await 与线程池请求处理还要等待同步身份存储调用识读 await run_in_threadpool(...);线程池不改变权限判断
try/finally 与上下文清理请求失败后不能残留上一名员工的审计身份识读设置/重置身份的成对操作,正常和异常路径都复验

先用可运行的局部规则理解服务端拒绝,不把它当完整登录系统:

class PermissionDenied(Exception):
    pass
 
def require_action(role, action):
    allowed = {"viewer": set(), "support": {"create", "cancel"},
               "admin": {"create", "cancel", "price", "pay"}}
    if action not in allowed.get(role, set()):
        raise PermissionDenied("当前角色不能执行此操作")
 
try:
    require_action("viewer", "cancel")
except PermissionDenied as exc:
    print(str(exc))

输入是已验证的角色与动作,结果是允许继续或抛出拒绝;此函数不负责查 Cookie,不接收前端自报管理员身份。我的独立变化是加入一个未知动作,先写拒绝断言,再核对真实 HTTP 请求失败后订单状态不变。只把按钮隐藏或只让这个小函数测试通过,都没有完成后台授权。

本日复用 W01 的业务对象、装饰器和自定义异常,把请求的身份校验与订单动作接起来。审计上下文由现有框架支架管理,重点解释进入、异常和清理路径,不要求临时设计第二套上下文框架。

能打开后台,不代表有权处理订单

我先用没有登录的请求访问订单接口,再用只读员工尝试取消订单。认证回答“是谁”,授权回答“能对哪个对象做什么”,审计记录“谁在什么时候做了什么”。前端按钮负责提示,服务端决定是否执行。

把取消按钮藏起来,是否足以限制只读员工?

不足,客户端可直接构造请求。后端每个受保护入口都要校验身份和当前角色权限,拒绝后不能改变订单。

员工与客户是不同身份

Customer 表示订单归属客户;后台员工负责代客查询或操作。业务请求中的 customer_id 不能决定操作人。操作人必须由已验证会话提供,后续 Agent 工具宿主也应沿用可信身份边界。

角色查询订单创建/取消待付款订单商品改价/模拟收款
viewer允许拒绝拒绝
support允许允许拒绝
admin允许允许允许

这是本项目的教学权限合同,并非所有企业的统一分工。登录和角色权限不自动获得客户归属或租户隔离能力;新增客户门户时要另加对象范围判断。

登录成功与有权取消订单为什么要分别检查?

认证确认员工是谁,授权确认当前角色能否做该动作。viewer 可以看列表,直接构造取消请求仍要在后端拒绝;隐藏按钮只是体验。

密码不按明文保存

为什么不直接保存员工输入的密码?一旦配置文件泄露,明文就能被直接使用。这里把口令经过密码哈希处理后保存;登录时用相同规则验证输入,服务端不需要“解密取回原密码”。

Salt(盐)是每份密码记录独立生成的随机值,让相同密码不直接得到相同结果;慢哈希提高批量猜密码的计算成本。先观察“相同口令、不同盐、都能正确验证”,再读算法名,不要求手写密码算法。

密码使用带随机盐的慢哈希保存,不能用 MD5 或单次 SHA256 代替密码哈希。当前教学实现使用 PBKDF2-HMAC-SHA256 和固定迭代参数,校验通过常量时间比较完成。用户不存在和密码错误采用相同提示,并限制重复尝试。

员工配置由教师在课前通过交互命令生成,学生使用自己创建的实验账号。配置文件保存密码哈希并排除 Git。这里采用受控配置文件管理少量员工,不包含注册、密码找回或企业 SSO。

浏览器、Cookie 与服务端会话

密码核对成功只发生在登录那一次。之后每次查订单,服务端凭什么认出同一个员工?我们沿着浏览器保存标识、请求携带标识、服务端查询会话的顺序来看。

sequenceDiagram
  participant B as 浏览器
  participant A as 应用
  participant R as Redis
  B->>A: 账号和密码
  A->>A: 校验密码与账号状态
  A->>R: 保存随机会话及过期时间
  A-->>B: HttpOnly Cookie 与 CSRF token
  B->>A: 携带 Cookie 的业务请求
  A->>R: 查会话
  A->>A: 校验角色、操作与 CSRF
  A-->>B: 业务结果或明确拒绝

会话 ID 是随机凭证,Redis 中保存其散列索引和服务端状态。登录时重新生成会话,注销删除服务端状态;清除浏览器 Cookie 不能代替服务端注销。空闲超时和绝对有效期分别限制无人使用的会话与持续使用的会话。

HttpOnly 限制脚本读取 Cookie;HTTPS 环境还要设置 Secure;SameSite 控制跨站携带范围。Cookie 自动携带意味着写请求要考虑 CSRF,本项目要求单独的 X-CSRF-Token。CORS 只是浏览器跨源读取规则,不能替代身份与权限。

浏览器 Cookie(会话载体)和服务端 Session(会话)有什么关系?

浏览器携带会话标识,服务端据此查可信身份与权限。注销需使服务端会话失效,并验证旧 Cookie 不能继续访问;不能只清空页面。

HttpOnly 限制脚本读取 Cookie,浏览器仍会自动携带它。需要结合 SameSite、可信来源和写请求 CSRF 校验;它们也不能代替防 XSS。

给所有受保护入口加同一规则

把昨天的“接口能调用”升级成今天的“谁能调用”。先照这三步判断,不要把所有失败都返回同一句话:

失败位置返回页面下一步
没有有效会话401(未认证)引导重新登录
会话有效,但角色无权执行403(无权操作)说明权限不足,不重复提交
会话服务故障,无法确认身份503(服务暂不可用)提示稍后再试,业务不执行

RBAC(基于角色的访问控制)把允许动作交给角色规则管理。后端读取可信会话中的角色,不接受请求体自报“我是管理员”。

/operations/orders、原 /orders、客户订单查询和库存查询都需要会话;写操作还需要角色与 CSRF。商品公开查询与健康检查按明确合同开放。不能只给新页面的入口加检查,让旧端点绕过。

无有效身份返回 401;身份有效但无操作权限返回 403;会话服务不可用返回 503 并拒绝操作。前端据此显示登录、权限或服务异常,不能把它们全部当成网络错误。

操作记录与事务保持一致

能拦住无权操作后,还要解释一次合法取消是谁做的。把业务修改与成功审计放在同一事务中,避免订单改了却找不到责任记录。

本项目将订单修改或商品改价的操作人、请求动作、时间写入 operation_audit,并与业务修改一起提交。提交失败时业务与记录一起回滚。普通访问日志用于诊断,业务审计用于追溯,二者不能混用;日志和审计都不记录明文密码或会话令牌。

一笔订单被取消后,企业需要查清哪些信息?

记录操作者、目标订单、动作、结果及关联请求,便于核对责任和排错;业务成功审计与状态更新应保持一致,不能记录密码或会话凭据。

现场验证

依次执行未登录查询、只读账号直接取消、客服改价、缺少 CSRF、注销后重放旧会话、身份服务故障。每个失败都核对业务状态没有变化。只有合法业务修改应留下相应操作记录。使用无权限账号做反例,比演示管理员一直成功更能证明权限边界。

参考:OWASP 授权、会话管理、密码存储。

注销只清理前端数据够吗?

不够。服务端会话仍有效时旧 Cookie 可继续使用。注销应删除 Redis 会话,并验证重放旧 Cookie 返回 401;业务操作身份从后端会话取得,不信前端传来的角色。

学生自己动手,AI 只能改你允许改的地方

我先保存当前可运行版本,再完成“只读账号直接请求取消订单,验证 403 和状态不变”。让 AI 一次只修改负责该行为的文件;修改前预测正常与失败结果,检查差异,再运行回归。每人需要解释一处接受或拒绝的建议。

错误与边界怎样验收

今天重点检查:前端隐藏按钮冒充授权、把 Customer 当员工、会话故障时放行。保留输入、预期、实际输出和修复后的结果,不能只贴全绿截图。未验证的外部依赖如实记为待验证。

留给下一阶段

用只读、客服和管理账号各走一条允许与拒绝路径,比只演示管理员成功更有用。明天把同一套身份和错误语义接到浏览器,让页面准确表达服务端结果。

官方资料