7.2 授权 Authorization 与安全实践 授权按声明裁决进与不进:角色授权回答"是不是仓管员",声明授权回答"部门是不是仓库一部",策略授权把多条规则组合成可复用的裁决单元——声明不满足时管线直接返回 403,业务代码保持干净。守门人周边还有四道工事要同时就位:防伪令牌、全站加密传输、密钥纪律、安全响应头。 身份进门之后的分权问题。观察哨本节站在授权站旁看两件事:裁决规则怎么表达才既精确又不散落;以及除了两站守门,管线还需要哪些安全配置才算"可以出门见人"。前半是授权的设计能力,后半是清单式的防御工事。 授权的三种表达力 从粗到细三种写法,表达力递增: 选型直觉:静态权限用角色或简单策略;规则涉及多个声明、外部数据或时间窗口,上自定义策略。
授权按声明裁决进与不进:角色授权回答"是不是仓管员",声明授权回答"部门是不是仓库一部",策略授权把多条规则组合成可复用的裁决单元——声明不满足时管线直接返回 403,业务代码保持干净。守门人周边还有四道工事要同时就位:防伪令牌、全站加密传输、密钥纪律、安全响应头。
身份进门之后的分权问题。观察哨本节站在授权站旁看两件事:裁决规则怎么表达才既精确又不散落;以及除了两站守门,管线还需要哪些安全配置才算"可以出门见人"。前半是授权的设计能力,后半是清单式的防御工事。
从粗到细三种写法,表达力递增:
// 一、角色授权:粗粒度,回答"是不是这个角色" [Authorize(Roles = "Admin")] public IActionResult Dashboard() => Content("管理面板"); // 二、简单策略:把"角色名"从代码里解放出来,改配置即可调整 builder.Services.AddAuthorization(o => { o.AddPolicy("CanViewReport", p => p.RequireRole("Admin", "Analyst")); }); [Authorize(Policy = "CanViewReport")] public IActionResult Report() => Content("报表页"); // 三、自定义策略:任意复杂规则,Requirement 加 Handler 两件套 public class DeptRequirement : IAuthorizationRequirement { public string Dept { get; } public DeptRequirement(string dept) => Dept = dept; } public class DeptHandler : AuthorizationHandler<DeptRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext ctx, DeptRequirement req) { // 读声明做裁决:部门声明匹配即通过 var dept = ctx.User.FindFirst("Dept")?.Value; if (dept == req.Dept) ctx.Succeed(req); return Task.CompletedTask; } } // 注册与使用 builder.Services.AddSingleton<IAuthorizationHandler, DeptHandler>(); builder.Services.AddAuthorization(o => o.AddPolicy("WarehouseOnly", p => p.AddRequirements(new DeptRequirement("仓库一部")))); [Authorize(Policy = "WarehouseOnly")] public IActionResult Stock() => Content("库存页"); // 部门不符 → 403,代码零侵入
选型直觉:静态权限用角色或简单策略;规则涉及多个声明、外部数据或时间窗口,上自定义策略。关键收益是裁决集中——权限逻辑收进策略定义,动作上只挂策略名,改规则不动业务代码。反过来,把权限判断散写在动作里(if 判断角色再 return 403)是反模式:规则演化时散落各处的 if 就是维护债务。
匿名例外与端点级挂法:受保护区域里个别页面放行用 AllowAnonymous 特性显式豁免;最小 API 用 RequireAuthorization。默认语义是"不挂就不查",所以页面项目常见做法是全局约定"先全拒、再显式放行公开页",把默认值调成安全侧。

背景:测试环境发现库存页任何登录用户都能打开,按需求只有仓库部门可见。页面动作上明明挂着特性。
操作:观察哨三步。第一步确认挂法:动作上是 [Authorize(Roles = "Operator")]——问题一现身,需求说"仓库部门",代码写的是"操作员角色",两套口径。第二步看主体:登录时签发的声明里有角色 Operator 与部门"仓库一部",但裁决只查角色。第三步改用部门策略(前文的 WarehouseOnly),角色码保留给其他场景。
结果:非仓库用户访问库存页得到 403;仓库用户正常。权限口径从"角色名硬编码"收敛到"部门声明裁决",后续新增仓库二部只改策略定义一处。解读:越权事故里"忘了挂特性"只占少数,多数是口径漂移——业务语言(部门)与实现语言(角色)没有对齐。策略层正是放"翻译后的业务规则"的位置。变式:需求复杂到"本部门且工龄满两年或主管"时,多个 Handler 各裁决一部分,策略组合它们——规则演进不需要改任何动作代码。
授权之外,一份上线前要过的安全清单。防伪令牌:表单页必须带(3.4 节的 TagHelper 自动插入、动作上 ValidateAntiForgeryToken 校验),防的是 CSRF——恶意站点诱导浏览器带着你的 Cookie 发请求;接口用 Bearer 令牌则天然免疫(恶意站点拿不到你的令牌)。传输加密:全站 HTTPS,开发环境也建议开着,Cookie 加 Secure 属性后只在加密信道传输。密钥纪律:7.1 节说过密钥走配置系统;口令存储必须散列加 盐,任何"明文口令表"都是定时炸弹。安全响应头:框架默认加一部分,补充两项常用的——内容安全策略头(限制页面可加载的资源来源,XSS 的纵深防御)与严格传输安全头(强制后续访问走 HTTPS)。
⚠️ 常见坑:把 403 当 401 排查(或反过来)。定位口诀:401 是"没身份"——查认证配置、令牌是否带上、票据是否过期;403 是"有身份没资格"——查策略定义与声明内容。先分清这两者,能省一半排障时间。
💡 关键直觉:授权站是"名单制门卫"——门口贴名单(策略),进门先对名单,不达标当场劝返。名单要贴在门口而不是藏在屋里(动作内 if 判断):看得见的规则才审计得动。