核心问题
一个主体应该拥有多少能力?
最小权限原则:
只授予完成任务所需的最小权限。
在软件工程里,这不仅是安全原则,也是伦理原则。
因为权限不是普通配置。权限是在分配权力:
- 谁能看见什么数据?
- 谁能修改什么资源?
- 谁能删除什么内容?
- 谁能代表别人行动?
- 谁能绕过规则?
- 谁能影响其他用户?
一个系统如果随便授予权限,本质上是在随便分配权力。
默认拒绝,而不是默认允许
最小权限的第一条是:
默认拒绝,明确允许。
坏模型:
function canEditCourse(accountId: string, courseId: string) {
if (isBlocked(accountId)) return false
return true
}
这意味着除了被明确阻止的人,其他人都能编辑课程。
好方向:
function canEditCourse(accountId: string, courseId: string) {
return (
isCourseOwner(accountId, courseId) ||
isAssignedCourseEditor(accountId, courseId) ||
isPlatformAdmin(accountId)
)
}
这里系统要求明确理由。
核心句:
权限判断应该寻找允许的理由,而不是寻找拒绝的理由。
Admin 不是万能神
很多系统有一个危险捷径:
if (user.role === "admin") return true
这很方便,但风险极高。
因为 admin 权限常常会扩张成:
- 看所有用户数据
- 改所有课程
- 删除所有订单
- 退款
- 修改权限
- 导出数据
- 冒充用户
- 关闭风控
这些能力风险完全不同,不应该被一个 admin 吞掉。
更好的做法是拆分能力:
canViewUserProfile(adminId, accountId)
canRefundPayment(adminId, paymentId)
canEditCourse(adminId, courseId)
canExportUserData(adminId)
canImpersonateUser(adminId, accountId)
并且为高风险能力加额外约束:
- 二次确认
- 审计日志
- 双人审批
- 时间限制
- 只读模式
- 数据脱敏
原则:
管理员不是一种身份,而是一组高风险能力的集合。
权限必须有上下文
不要写:
user.role = "manager"
因为 manager 必须回答:
管理什么?
更清楚:
type OrganizationMembership = {
accountId: string
organizationId: string
role: "owner" | "manager" | "member"
}
或者:
type CourseStaffAssignment = {
accountId: string
courseId: string
role: "editor" | "reviewer"
}
权限没有上下文,就会变成全局权力。
核心句:
权限必须绑定资源范围。
RBAC 和 ABAC
常见权限模型有两类。
RBAC:Role-Based Access Control
基于角色授权。
例如:
organization owner 可以邀请成员
course editor 可以编辑课程
platform support 可以查看工单
优点:
- 简单
- 好理解
- 适合组织权限
缺点:
- 角色容易膨胀
- 特殊情况多时会变复杂
ABAC:Attribute-Based Access Control
基于属性授权。
例如:
account.organizationId == course.organizationId
account.department == "training"
course.status == "draft"
request.ip in trusted_range
优点:
- 灵活
- 适合复杂条件
缺点:
- 难理解
- 难审计
- 容易出现隐式规则
多数业务系统会混合使用:
function canAssignCourse(accountId, organizationId, courseId) {
return (
hasOrganizationRole(accountId, organizationId, "manager") &&
organizationHasCourseAccess(organizationId, courseId)
)
}
这里既有角色,也有属性条件。
重点不是选择名词,而是:
权限规则要可读、可测试、可审计。
权限不是前端按钮
隐藏按钮不是权限控制。
坏做法:
if (user.isAdmin) {
showDeleteButton()
}
然后后端接口没有检查。
真正权限必须在后端执行:
async function deleteCourse(accountId: AccountId, courseId: CourseId) {
if (!(await canDeleteCourse(accountId, courseId))) {
throw new ForbiddenError("COURSE_DELETE_FORBIDDEN")
}
await courses.delete(courseId)
}
前端权限只改善体验,不能提供安全。
核心句:
UI 可以提示权限,后端必须执行权限。
权限和审计绑定
高风险权限必须留下审计。
例如:
- 删除课程
- 手动退款
- 导出用户数据
- 修改用户权限
- 冒充用户
- 解封账号
审计日志应该记录:
type AuditLog = {
actorId: string
action: string
resourceType: string
resourceId: string
reason?: string
occurredAt: Date
requestId: string
}
尤其是后台操作,要知道:
- 谁做的?
- 对什么资源做的?
- 为什么做?
- 什么时候做?
- 从哪里发起?
原则:
权力越大,证据越多。
Service Account 也要最小权限
最小权限不只针对人,也针对服务。
坏做法:
所有服务共用一个 database admin credential
风险是一个服务被攻破,所有数据都暴露。
更好的做法:
- 读服务只给读权限
- worker 只给需要的表权限
- analytics 只能读脱敏数据
- billing service 只能操作支付相关表
- CI token 只给部署需要的权限
Service account 应该回答:
这个服务为了完成职责,最少需要哪些能力?
临时权限必须自动过期
临时权限如果不会过期,就不是临时权限。
例如:
给某工程师临时 production database access 排查事故。
应该有:
- 申请理由
- 审批人
- 过期时间
- 操作审计
- 自动回收
否则临时权限会变成永久后门。
核心句:
临时权限必须带过期时间。
最小权限的坏味道
- 到处都是
if admin return true。 - 一个角色拥有大量无关能力。
- 权限只在前端判断。
- 后台操作没有审计日志。
- service account 使用超级权限。
- 临时权限不会过期。
- 权限错误只能靠客服或用户发现。
- 不知道谁能导出用户数据。
- 权限规则散落在各个 handler 里。
- 没有测试覆盖拒绝路径。
最小权限检查清单
设计权限时问:
- 这个主体是谁?人、服务、组织还是外部应用?
- 它要完成什么任务?
- 完成任务所需的最小能力是什么?
- 权限绑定在哪个资源范围?
- 默认是否拒绝?
- 拒绝时是否有稳定 reason?
- 高风险操作是否有审计?
- 是否需要二次确认或审批?
- 临时权限是否自动过期?
- 是否有测试覆盖允许和拒绝路径?
小结
- 最小权限是安全原则,也是伦理原则。
- 权限是在分配权力。
- 默认拒绝,明确允许。
- 管理员不是万能神,而是一组高风险能力。
- 权限必须绑定上下文和资源范围。
- UI 可以提示权限,后端必须执行权限。
- 权力越大,证据越多。
- 人和服务都应该遵守最小权限。
- 临时权限必须自动过期。