会话、对象权限与批量后台
把业务动作放入显式服务,页面和 Admin 复用同一入口:
# tickets/services.py
from django.core.exceptions import PermissionDenied
from django.db import transaction
from .models import Ticket
@transaction.atomic
def close_ticket(ticket, actor):
if ticket.assignee_id != actor.pk and not actor.has_perm("tickets.change_ticket"):
raise PermissionDenied
ticket.status = Ticket.Status.CLOSED
ticket.save(update_fields=["status", "updated_at"])
return ticket2
3
4
5
6
7
8
9
10
11
12
13
# tickets/admin.py
@admin.action(description="Close selected tickets")
def close_selected(modeladmin, request, queryset):
for ticket in queryset.select_related("assignee"):
close_ticket(ticket, request.user)2
3
4
5
深水边界
会话 Cookie 通常只保存会话键;服务端会话仍需过期、轮换、撤销和多设备策略。登录后 Django 会轮换会话标识以降低固定攻击。权限缓存可能让同一 User 实例看不到刚授予的权限,测试时重新获取对象。
Admin 批量 action 可能部分成功、持锁过久或触发大量副作用。数据量大时改为后台作业,保存操作人、过滤条件、目标快照和审计结果。
验收
页面与 Admin 共享业务不变量;越权被服务层拒绝;会话过期和撤权路径有测试;批量动作有数量上限与审计。
练习(先独立完成)
- 为关闭动作增加关闭者与关闭时间审计。
- 演练用户权限在活跃会话中被撤销。
- 让 Admin action 对无权工单给出明确失败汇总。
深度加餐:会话与对象权限是两扇不同的门
登录只证明“你是谁”;对象权限还要回答“你能改哪一条记录”。
先做一个可观察实验
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from .models import Ticket
def update_ticket(request, pk):
ticket = get_object_or_404(Ticket, pk=pk)
if ticket.owner_id != request.user.id and not request.user.is_staff:
raise PermissionDenied
# 进入表单校验与保存
return ...2
3
4
5
6
7
8
9
10
预期:普通用户只能修改自己的工单;staff 走明确的管理分支,权限判断在读取对象后、写入前完成。
原理下潜与工程边界
登录后 session key 应轮换,注销要失效旧会话;对象权限应落在 queryset 或 service 层,不能只依赖模板隐藏按钮。Admin action 要处理选中为空、部分失败和审计记录。
只在前端隐藏按钮不是安全控制;session 数据默认存储在数据库或缓存,容量和过期策略要监控;跨站请求仍需 CSRF 与 SameSite 配合。
反例排错路线
测试匿名、普通用户、对象所有者、staff、已禁用用户五种身份;失败时记录 request.user、对象 owner_id 和权限规则命中项。
练习加码(仍不提供答案)
加码:实现批量转派 action,要求只能转派当前用户拥有的工单,成功和失败数量都写入审计日志。
验收时请补充:正常路径、权限或失败路径、以及一条能证明事务/资源/配置边界没有被绕过的测试。
继续查阅:Django 官方文档(外部资料已收录于证据账本) · Django 安全清单(外部资料已收录于证据账本)
课程合同:会话、对象权限与后台动作
学习目标
学完本篇,读者应能把「用户 A 不能读取或批量修改用户 B 的工单」拆成输入、可信边界、Django 机制和可复查证据;同时能说明 Django 6.0 与 5.2 LTS 的差异不会改变业务不变量。
心智模型
请求/数据输入 → 边界检查 → Django 核心机制 → 可观察输出 → 恢复动作。配置、数据库提交和外部副作用分别拥有自己的生命周期;没有证据的“应该可以”不算完成。
本地机制图
图中的“输入”是不可信的 HTTP、表单、任务或配置;“边界”负责身份、格式、权限、大小、超时与版本;“机制”才是 URL、View、ORM、事务、缓存、Tasks 或 ASGI;“证据”是测试、响应、日志、查询计划和发布记录。
最小实验
在全新虚拟环境中运行:
python -m django --version
python manage.py check
python manage.py test2
3
实验只验证一个机制:输入一条有效值,再输入一条空值、重复值或无权限值。每次实验都记录 Django 版本、数据库、命令和退出码。
完整场景
把实验接到 tickets 工单流:请求携带用户身份和工单 ID,经过当前专题负责的边界,完成一次读或写,并留下响应状态、数据库变化、日志字段和清理动作。完整场景必须能第二次执行,或明确说明幂等键、事务回滚、缓存失效和任务重试策略。
运行输出
成功路径应出现:
Django 6.0.x
System check identified no issues (0 silenced).
OK2
3
失败路径要保留真实信号,例如 400(输入无效)、403(权限/CSRF)、404(资源不存在)、405(方法不允许)或数据库约束异常;不要把失败改写成含糊的 200。
失败边界
至少验证:空输入、重复提交、匿名用户、越权对象、并发写入、下游不可用、超时和版本不兼容。记录“谁拒绝、何时拒绝、状态是否回滚、重试是否安全”。开发模式的 traceback 不得进入生产响应,缓存不得跨用户复用敏感结果。
原理与排错
先确认版本和 settings,再用 manage.py check 定位启动问题;接着确认 URL/任务入口、输入解析和权限;最后检查 SQL、事务提交、响应和副作用。若结果异常,用最小复现打开 SQL/请求 ID/任务 ID 观测,沿「复现 → 缩小 → 观测 → 修复 → 回归」闭环,不靠重装依赖猜测。
练习(正文不泄答案)
- 为上述完整场景补一条正常路径和两条失败路径测试,并记录完整输出。
- 画出本篇机制图,标出身份 owner、数据提交点、缓存/任务副作用和恢复动作。
- 故意制造一次重复、越权或超时,说明应返回的状态、日志字段和是否可重试。
- 写一条 Django 5.2 LTS 兼容验收标准,再说明 Django 6.0 的新能力是否需要条件分支。
验收标准:命令可重复、输出可复查、正文没有练习答案;完成后再打开本阶段答案册。
答案册
本阶段答案册提供参考实现、关键推理、失败边界和替代方案;先独立完成练习再对照。
下一篇
下一篇是06-apis-and-contracts/01-json-api-contracts,继续沿同一条输入—边界—机制—证据链前进。