using Admin.NET.Core; using Admin.NET.Core.Service; using Admin.NET.Plugin.AiDOP.Const.S8; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8; /// /// S8-P0-4-API-AUTHORIZATION-1:S8 接口能力级授权门。 /// /// 为什么不能直接复用平台的路由名匹配(这是本方案的唯一设计前提,先读这段): /// JwtHandler.CheckAuthorizeAsync 把 /// HttpContext.Request.Path 逐字转成权限名(/:)再与 /// SysMenu.Permission 比对。平台自身接口全是 /api/sysPos/delete 这类 /// 无路径参数的动词式路由,于是 sysPos:delete 天然对得上。 /// /// S8 是 RESTful 路由,两点结构性不兼容: /// /// 路径参数/api/aidop/s8/exceptions/1329909430019/claim 会被转成 /// aidop:s8:exceptions:1329909430019:claim —— 含实例 id, /// 任何静态权限码永远不可能相等; /// 命名体系:S8 权限码是能力语义(s8:exception:claim), /// 而非路由字面量(aidop:s8:exceptions:…)。 /// /// /// 再加上 JwtHandler 末行 allBtnPermList.TrueForAll(u => !routeName.Equals(u)) /// —— 未登记路由默认放行,于是全部 S8 接口实测为「登录即可调用」: /// 2026-09-02 以业务账号 UATExceptionA 实测 PUT /config/watch-rules/{id}/params /// 与 POST /config/operator-bindings 均直达业务校验返回 400(而非 403)。 /// /// 本类做什么:只替换「路由名 → 权限码」这一步为显式声明, /// 其余全部复用平台既有设施 —— 权限仍存 (Type=Btn).Permission, /// 仍经 SysRoleMenu 授予角色,仍由平台的 /// (含缓存)取用户权限集。 /// 不新造权限存储、不改 JwtHandler(那是所有模块共用的平台代码,改它会外溢到 S0–S7)。 /// /// 失败语义:未持有声明的权限码 → 直接 403, /// 在进入 Controller Action 与任何 Service 之前返回,DB 零写入。 /// /// 超管豁免:与 JwtHandler.CheckAuthorizeAsync 完全同口径 /// (),避免出现「平台放行、S8 拦截」的不一致。 /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public sealed class S8PermissionAttribute : Attribute, IAsyncAuthorizationFilter { /// 本 Action 要求的能力码,必须取自 常量。 public string Code { get; } public S8PermissionAttribute(string code) { if (string.IsNullOrWhiteSpace(code)) throw new ArgumentException("S8 权限码不能为空", nameof(code)); Code = code; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { // 认证本身由平台管道负责;这里只判「已认证身份是否具备该能力」。 // 未认证时交回平台的 401 语义,不在此重复判定。 var user = context.HttpContext.User; if (user?.Identity?.IsAuthenticated != true) return; var services = context.HttpContext.RequestServices; // 超管豁免:与 JwtHandler 同口径。 var userManager = services.GetService(); if (userManager is { SuperAdmin: true }) return; var menuService = services.GetService(); if (menuService == null) { // 拿不到平台权限服务时按拒绝处理(fail-closed)。 // 绝不 fail-open —— 本模块的事故类型全部是「本不该放行却放行了」。 context.Result = Forbid(); return; } List owned; try { owned = await menuService.GetOwnBtnPermList(); } catch { context.Result = Forbid(); return; } if (owned != null && owned.Contains(Code, StringComparer.OrdinalIgnoreCase)) return; context.Result = Forbid(); } /// /// 统一 403 出参。文案不回显所需权限码与用户已有权限,避免把权限拓扑透给调用方。 /// private static ObjectResult Forbid() => new(new { message = "没有权限执行该操作" }) { StatusCode = StatusCodes.Status403Forbidden }; }