AIGC标识 C#特性笔记:实现特殊权限验证

实验环境:C#、.NET 5、ASP.NET Core MVC、ASP.NET Boilerplate / ABP.Zero 6.3.0

这次学习从一个问题开始:在 Controller 上写下 [AbpMvcAuthorize(...)],为什么就能控制访问权限?如果希望验证逻辑由自己编写,并且验证过程需要读取 Action 的参数,应该怎样实现?

我使用 OrderEntryController 作为实验入口,编写了 YOLinkPermissionCheck Demo。它接收三个参数名称,在 Action 执行前取得实际参数值,再调用独立的业务验证服务。本文记录需求如何逐步明确、代码如何协作,以及实践中需要分清的几个概念。

文中的“当前实现”以这次整理时的仓库代码为准;标注为“教学示例”或“后续扩展”的代码不代表已经修改到项目中。

1. 学习从哪些问题开始

这次需求逐渐收敛为下面这些规则:

学习过程中的问题 Demo 最终采用的做法
能不能像普通特性一样使用? 用 [YOLinkPermissionCheck(...)] 标注 MVC Controller 或 Action。
写在 Controller 上能否保护其 Action? 可以,每个 MVC Action 都会应用该验证。
能否读取普通参数与 DTO? 在 Model Binding 后读取 ActionArguments,支持普通参数和 DTO 的直接公共属性。
Action 有不同数量、类型的业务参数怎么办? 只按名称提取三项验证参数,其余参数仍由 Action 使用。
游客能否调用? AllowGuest 默认 true,允许未登录请求进入自定义验证,成功后才执行 Action。
必须提交哪些内容? 每次请求提供 UserId、EmployeeID、Password。
验证规则由谁定义? 在 YOLinkPermissionChecker.CheckAsync 中手动编写业务规则。
GET 能否用于实验? 初版拒绝 GET;为演示普通参数 GET,后来增加 AllowGet = true 显式开启。
首次验证成功后是否免验? 不会,每个请求重新验证。
能否查询员工信息并记录手机号? 可以,通过 Repository 查询,在身份和权限全部通过后调用日志接口。

OrderEntryController 只是学习入口。实际使用时,每个受保护的 Action 都需要提供三项必要参数,但额外业务参数可以各不相同。

2. AbpMvcAuthorize 为什么能控制权限

例如:

[AbpMvcAuthorize(AppPermissions.Pages_OrderEntry)]
public class OrderEntryController : SISControllerBase
{
    // 省略业务方法。
}

这里声明了访问所需的权限名称。ABP 的 MVC 授权机制读取这些声明,结合当前请求的登录身份及其权限进行检查。未满足要求时,请求不会进入目标 Action。

可以把授权过程概括为:读取权限声明 → 确定当前用户 → 检查权限定义和用户授权 → 允许或拒绝继续执行。角色授权、用户单独授权等由 ABP 的权限系统处理,不需要在每个 Action 中重复查询。

本项目中可以沿以下位置学习:

位置 作用
AppPermissions.cs 定义权限名称常量,例如 Pages.OrderEntry。
AppAuthorizationProvider.cs 注册权限及其层级、适用的租户侧。当前订单录入权限注册为 Host 侧权限。
PermissionChecker.cs 项目的权限检查器,继承 ABP 提供的实现。
UserManager.cs 用户管理入口,继承 ABP 用户管理功能。

AbpMvcAuthorizeAttribute 属于 Abp.AspNetCore.Mvc.Authorization,实现来自 Abp.AspNetCore NuGet 包;IPermissionChecker 位于 Abp.Authorization。它们不在这次新增的六个文件中。

这份 Demo 与上述机制的差别在于:调用者可能尚未登录,需要用请求携带的三项数据执行自定义业务验证,然后检查验证得到的用户是否拥有权限。

3. 它是 C# 特性,还是 .NET 特性

准确地说,YOLinkPermissionCheck 是使用 C# 编写、基于 .NET Attribute 机制,并接入 ASP.NET Core MVC Filter 的自定义特性。

层面 在 Demo 中的表现
C# 语法 使用方括号标注;YOLinkPermissionCheckAttribute 可省略 Attribute 后缀。
.NET Attribute 机制 类型继承 System.Attribute,把配置附加在类或方法上。
ASP.NET Core MVC 识别 Filter 元数据,创建并调用过滤器。
业务代码 CheckAsync 决定如何查询用户、判断员工编号、密码和权限。

仅继承 Attribute 并标注方法,不会自动在方法调用前插入验证代码。当前 Demo 能运行,是因为 MVC 会识别其 IFilterFactory 并执行创建出来的 Filter。直接在普通 C# 代码中调用 Controller 方法,不会自动走这条 MVC 请求管线。

4. 特性的三个字符串传递了什么

[YOLinkPermissionCheck("UserId", "EmployeeID", "Password")]

三个字符串是查找参数时使用的名称,并不是本次请求的用户编号、员工编号和密码值。Attribute 的这些字符串配置需要能够在编译时确定;实际值要等请求到达并完成参数绑定后才能取得。

因此,下面的普通参数和 DTO 可以复用同一种标注:

public IActionResult Edit(long UserId, string EmployeeID, string Password, int orderId)
public IActionResult Edit(OrderEditDto input)

第二种写法要求 OrderEditDto 直接包含这三个同名属性。当前解析器会查找属性 EmployeeID,没有实现 input.EmployeeID 这样的路径表达式解析,因此当前用法仍然填写 "EmployeeID"。

其他配置的含义如下:

配置 当前默认值 含义
AllowGuest true 自定义 Filter 不要求请求预先登录,但仍须通过三项数据的验证。
AllowGet false 默认拒绝 GET;设为 true 才允许 GET 进入验证。它不代替 [HttpGet] 或 [HttpPost] 路由约束。
PermissionCode 未设置 未设置时生成 ControllerName.ActionName;设置后使用指定编码。

默认生成的权限编码只是字符串,不会自动在 ABP 注册权限。当前两个测试 Action 显式使用已注册的 AppPermissions.Pages_OrderEntry。如果继续使用 ABP 验证,编码需要与 ABP 中实际定义的权限一致;自定义业务实现则可以自行解释该编码。

5. 为什么选择 IAsyncActionFilter

这次验证依赖 Action 的普通参数和 DTO,因此需要在 MVC 完成 Model Binding 后执行。

请求处理顺序可以简化理解为:

路由、认证与授权中间件
    ↓
MVC Authorization Filters
    ↓
Resource Filters 的前置阶段
    ↓
Model Binding
    ↓
Action Filters 的前置阶段 ← 本 Demo 在这里读取参数
    ↓
Action 执行
    ↓
后续 Filter 与结果处理

这是帮助理解的简化顺序,前面的阶段可以提前终止请求。IAsyncActionFilter 提供的 ActionExecutingContext.ActionArguments 保存了已绑定的参数,避免在验证器中重复读取和解析请求正文。

本 Demo 通过设置 context.Result 并直接返回来阻止 Action;验证成功后调用 await next(),才允许后续 Filter 和 Action 继续执行。

Attribute 的 Order = -1000 用来影响 Action Filter 的相对执行顺序。它不会让 Action Filter 跑到 Authorization Filter 或 Model Binding 前面。

6. 六个核心文件怎样协作

六个文件分布在 Application.Shared、Application 和 Web.Core。接口、输入模型和结果模型负责约定数据,实际的请求拦截与业务判断由 Filter 和 Checker 执行。

文件 所属层 职责
IYOLinkPermissionChecker.cs Application.Shared 定义异步验证入口 CheckAsync,供 Filter 依赖。
YOLinkPermissionContext.cs Application.Shared 传递三项验证数据、权限编码、Controller 名称和 Action 名称。
YOLinkPermissionResult.cs Application.Shared 定义成功结果、失败原因和已验证的用户信息。
YOLinkPermissionChecker.cs Application 放置可手动修改的业务规则,以及 Repository 和日志调用。
YOLinkPermissionCheckAttribute.cs Web.Core 保存标注配置,通过 IFilterFactory 创建 Filter;同文件还定义了 YOLinkPermissionCheckOptions。
YOLinkPermissionCheckFilter.cs Web.Core 读取参数、构造验证上下文、调用 Checker、处理结果并决定是否放行。

测试入口另有两个文件:

Attribute 如何取得业务服务

Attribute 实现 IFilterFactory。MVC 调用 CreateInstance 时,它先把配置放进 Options,再创建 Filter:

var options = new YOLinkPermissionCheckOptions(
    UserIdParameterName,
    EmployeeIdParameterName,
    PasswordParameterName,
    PermissionCode,
    AllowGuest,
    AllowGet);

return (IFilterMetadata)ActivatorUtilities.CreateInstance(
    serviceProvider,
    typeof(YOLinkPermissionCheckFilter),
    options);

这里的 Options 是显式传入的;Filter 构造函数需要的 IYOLinkPermissionChecker 则从 DI 容器解析。IsReusable = false 表示不把创建的 Filter 标记为可跨请求复用;请求数据不应保存在 Attribute 中。

验证器实现 ITransientDependency,由 SISApplicationModule.cs 的 RegisterAssemblyByConvention 按 ABP 约定注册。它没有继承 IApplicationService,因此不是自动公开的 AppService API。

以后可以直接修改 CheckAsync,也可以实现并替换 IYOLinkPermissionChecker 的 DI 注册。仅新增第二个实现类,不等于已经明确替换了默认实现。

7. 一次请求的完整执行过程

MVC 先根据 Attribute 创建 Filter。前面的授权等阶段通过后,Model Binding 填充 Action 参数,随后进入以下流程:

flowchart TD A[请求完成 Model Binding] --> B[YOLinkPermissionCheckFilter] B --> C{访客和 GET 配置允许?} C -->|否| R[设置结果并结束] C -->|是| D[读取三个参数并检查有效性] D -->|失败| R D -->|成功| E[生成 PermissionCode 和 Context] E --> F[IYOLinkPermissionChecker.CheckAsync] F --> G[用户状态及员工编号校验] G --> H[查询 EmployeeInfo] H --> I[密码和 Action 权限校验] I --> J{全部通过?} J -->|否| K[返回 Fail 并阻止 Action] J -->|是| L[记录手机号日志并返回 Success] L --> M[写入 HttpContext.Items] M --> N[调用 next 继续执行 Action]

实际业务校验采用提前返回:任一步失败,后续检查及手机号日志都不会执行。Checker 返回成功之后,Filter 保存的是 YOLinkPermissionResult,不是包含 Password 的输入上下文。

8. 参数解析支持什么

每个验证字段独立查找,当前顺序是:

  1. 在 ActionArguments 中按名称查找,忽略大小写。
  2. 找到普通参数后使用其值;值为空时失败,不再回退到 DTO。
  3. 未找到普通参数时,遍历其他参数对象的直接公共可读属性。
  4. DTO 属性只能匹配一处;找不到、找到多处或值为空时拒绝。
  5. UserId 必须能够转换为大于 0 的 long,另外两项转换为字符串后不能是空白。

普通参数优先意味着:如果 Action 同时有 EmployeeID 参数和 DTO 的 EmployeeID 属性,当前实现使用普通参数,不会继续检查两者是否一致。多个 DTO 之间的重名冲突,只在没有匹配到普通参数时判断。

以下是教学示例,说明额外业务参数不受数量与类型一致性的限制:

public IActionResult Edit(
    long UserId,
    string EmployeeID,
    string Password,
    int orderId,
    string mode,
    decimal quantity)
{
    return Ok();
}

public IActionResult Submit(
    OrderSubmitDto input,
    bool sendNotification,
    int[] detailIds)
{
    return Ok();
}

第二个方法的 OrderSubmitDto 直接包含三项验证属性即可。三个字段也可以分布在普通参数和 DTO 中,查找时分别应用同样规则。

额外的 orderId、quantity 等仍由 MVC 传给 Action,但当前没有装入 YOLinkPermissionContext。如果授权规则需要根据订单所属人或金额判断,就需要显式扩展 Context 和提取逻辑。

当前没有实现嵌套路径、集合元素或字典键的解析,也不应把任意对象都视为受支持的 DTO。普通的公共属性 DTO 是这次 Demo 的目标用法。

9. 普通参数 GET 与 DTO POST 的实验

普通参数 GET

当前 GetCurrentUser 的标注与签名是:

[HttpGet]
[YOLinkPermissionCheck(
    "UserId",
    "EmployeeID",
    "Password",
    PermissionCode = AppPermissions.Pages_OrderEntry,
    AllowGet = true)]
public async Task<ActionResult> GetCurrentUser(
    long UserId,
    string EmployeeID,
    string Password)

调用结构如下,示例值仅为占位:

GET /app/OrderEntry/GetCurrentUser?UserId=1&EmployeeID=DEMO001&Password=demo-placeholder

验证通过后,Action 返回用户编号、员工编号、姓名、姓氏和邮箱。AllowGet = true 只放开 GET 限制,不会跳过密码或权限验证。

本 Demo 当前的 Password 确实位于 QueryString,没有实现之前讨论过的 Header 绑定方式。URL 可能进入访问日志、历史记录等位置,因此这个例子用于观察 GET 参数绑定;实际业务更适合通过 HTTPS 下的 POST 正文或另外设计的凭据机制传递。

DTO POST

当前 GetCurrentEmail 的标注与签名是:

[HttpPost]
[YOLinkPermissionCheck(
    "UserId",
    "EmployeeID",
    "Password",
    PermissionCode = AppPermissions.Pages_OrderEntry)]
public async Task<ActionResult> GetCurrentEmail(GetCurrentEmailInputDto input)

DTO 定义如下:

public class GetCurrentEmailInputDto
{
    public long UserId { get; set; }
    public string EmployeeID { get; set; }
    public string Password { get; set; }
}

这个 MVC Action 当前没有 [FromBody]。按现有方法声明,表单提交是匹配默认绑定方式的示例:

POST /app/OrderEntry/GetCurrentEmail
Content-Type: application/x-www-form-urlencoded

UserId=1&EmployeeID=DEMO001&Password=demo-placeholder

请求还需要满足项目中已启用的防伪验证等要求,以上只展示业务参数部分。验证通过后,Action 构造包含 EmailAddress 的 JSON 对象;实际响应是否有 ABP 包装,以运行时配置为准。

如果希望使用 application/json,需要明确采用 JSON Body 绑定,例如下面的教学变体:

public async Task<ActionResult> GetCurrentEmail(
    [FromBody] GetCurrentEmailInputDto input)
{
  "userId": 1,
  "employeeID": "DEMO001",
  "password": "demo-placeholder"
}

整理时需要纠正前面讨论中的一个误解:使用 DTO 并不自动意味着能够接收 JSON。请求格式、MVC 绑定声明和 Filter 参数读取是相互衔接的三件事。这个 JSON 变体尚未写入当前 Demo。

10. 写在 Controller 上怎样作用于所有 Action

以下是 Controller 级教学示例:

[Area("app")]
[YOLinkPermissionCheck(
    "UserId", "EmployeeID", "Password",
    PermissionCode = AppPermissions.Pages_OrderEntry,
    AllowGet = true)]
public class OrderEntryController : SISControllerBase
{
    [HttpGet]
    public IActionResult GetCurrentUser(
        long UserId, string EmployeeID, string Password)
    {
        return Ok();
    }

    [HttpPost]
    public IActionResult GetCurrentEmail(GetCurrentEmailInputDto input)
    {
        return Ok();
    }
}

Controller 上的标注会参与其所有 MVC Action 的 Filter 管线。此例两个方法无需再各自添加相同特性,业务参数结构仍可不同。

如果保留 Index() 这样的无参数方法,它也会被检查,并因无法取得三项验证数据而失败。即使在 URL 上补上三项字段,只要 MVC 没有把它们绑定为 Action 参数或 DTO 属性,当前 Filter 也不会直接去 QueryString 补取。

Controller 级固定 PermissionCode 会让其 Action 检查同一个权限;不指定时,才会按每个 Action 生成不同编码。当前实现没有提供“Action 配置覆盖 Controller 配置”的逻辑。如果两处都标注,可能形成两次 Filter 执行;AllowMultiple = false 不等于 Controller 与 Action 之间自动去重。

当前仓库仍在两个测试 Action 上使用特性,Controller 级示例处于注释状态。

11. 哪些代码应该由业务开发者编写

YOLinkPermissionChecker.CheckAsync 是业务扩展入口。以下规则来自本次实验,均可以按实际业务替换:

代码中的注释标记 当前规则
业务代码位置 1 根据 UserId 查用户,并要求 IsActive 为真。
业务代码位置 2 EmployeeID.Trim() 与 User.UserName 忽略大小写比较。
业务代码位置 2a 查询 EmployeeInfo,要求员工存在且员工编号与用户匹配。
业务代码位置 3 使用 Identity 的 PasswordHasher 检查用户密码哈希。
业务代码位置 4 按已验证用户的 UserIdentifier 检查 ABP 权限。
业务代码位置 5 全部通过后,记录员工手机号及本次访问的权限信息。

例如,通过下面的输入上下文,业务代码能知道正在验证谁、访问哪个 Action:

Task<YOLinkPermissionResult> CheckAsync(YOLinkPermissionContext context);

context 包含 UserId、EmployeeID、Password、PermissionCode、ControllerName 和 ActionName。失败时返回对应的 Fail(...),全部验证通过才返回 Success(...),成功结果应填写服务端确认的用户身份。

业务实现中的一个版本细节也值得记录:本项目的 User 继承 ABP 的 AbpUser<User>,密码哈希属性是 User.Password。最初直接使用常见的 PasswordHash 属性名导致编译错误,核对依赖后才修正。属性名包含 Password,并不表示数据库里保存的是明文密码。

当前密码规则使用的是用户账户密码,并未改为 EmployeeInfo.ModuleAccessPassword。同样,EmployeeID 与 UserName 的对应关系是本项目的业务约定,不是 .NET 的通用要求。

12. 根据员工编号查询手机号并记录日志

验证器构造函数中注入:

IRepository<EmployeeInfo> employeeInfoRepository,
ILogger<YOLinkPermissionChecker> logger

这里只使用泛型 Repository 已提供的查询能力,无需新增空的 Repository 接口。当前查询发生在员工编号与用户匹配之后:

var employeeInfo = await _employeeInfoRepository.FirstOrDefaultAsync(
    x => x.EmployeeId == context.EmployeeID.Trim());

if (employeeInfo == null ||
    !string.Equals(employeeInfo.EmployeeId, user.UserName,
        StringComparison.OrdinalIgnoreCase))
{
    return YOLinkPermissionResult.Fail(
        YOLinkPermissionFailureReason.EmployeeMismatch);
}

密码、权限检查也通过后,才执行日志调用:

_logger.LogInformation(
    "YOLink 权限验证成功,UserId={UserId},EmployeeID={EmployeeID},EmployeePhone={EmployeePhone},PermissionCode={PermissionCode},ControllerName={ControllerName},ActionName={ActionName}",
    user.Id,
    context.EmployeeID,
    employeeInfo.EmployeePhone,
    context.PermissionCode,
    context.ControllerName,
    context.ActionName);

这是使用占位符的日志模板。当前实验按确认方案记录完整 EmployeePhone;手机号为空没有被定义为验证失败。日志是否实际写入、保存到哪里,由宿主的日志 Provider 和级别配置决定,调用 LogInformation 并不等于已经验证文件落盘。

这条日志表示“本次权限验证成功”。它发生在 Action 执行前,不能作为“业务操作成功”的证明。

这段日志没有写入 Password,但不能据此推断整个请求处理过程都不会记录密码。项目启用了匿名用户审计,当前 Demo 的 DTO 尚未对 Password 增加审计排除;GET URL 日志也属于另外的记录路径。正式使用时,应分别处理敏感参数审计与手机号脱敏,而不能只检查这一条日志模板。

13. 游客验证通过后,是否就登录了

没有。当前验证成功只执行了:

context.HttpContext.Items[
    YOLinkPermissionContext.VerifiedUserItemKey] = result;

await next();

HttpContext.Items 只在当前请求内有效。后续 Action 可以读取验证结果,例如下面的教学代码:

var verifiedUser = HttpContext.Items[
    YOLinkPermissionContext.VerifiedUserItemKey] as YOLinkPermissionResult;

当前 Demo 没有创建登录 Cookie、签发 Token,也没有把已验证身份设置到 HttpContext.User 或 ABP Session。再次请求同一个 Action,仍要提交三项数据并重新验证;即使调用者原本已经登录,也不会自动跳过三项验证。

因此,后续业务如果读取 AbpSession.UserId,不能假定它就是此次通过三项数据验证的用户。当前结果里的身份与宿主认证身份是两份不同的信息。

如果以后希望首次验证后免传 Password,需要另行设计 Token 或 Cookie 的签发、有效期和撤销规则。这属于后续扩展,本次没有实现。

AllowGuest = true 也只控制本 Filter 的登录前置检查,不会自动绕过 [AbpMvcAuthorize]、ASP.NET Core 的 [Authorize]、防伪验证或其他上游限制。游客必须先能到达这个 Filter,才有机会执行自定义验证。

14. 失败结果与实际 HTTP 响应

当前 Filter 根据业务结果选择不同的 MVC Result:

情况 当前处理
参数缺失、无效、无法确定 Controller/Action,或 GET 未允许 BadRequestObjectResult,状态码 400。
Checker 返回 InvalidRequest 或空结果 BadRequestObjectResult,状态码 400。
用户不存在或未启用、员工信息不匹配、密码错误 UnauthorizedResult,状态码 401;不向客户端逐项暴露失败原因。
用户身份校验通过但权限不足 ForbidResult,由宿主认证处理器执行禁止访问响应。
AllowGuest = false 且调用者未登录 ChallengeResult,由宿主认证处理器发起认证挑战。
验证成功 写入验证结果,并调用 next()。

ForbidResult 表达禁止访问的语义,但不能简单保证客户端一定收到裸的 403。Cookie 认证处理器可能产生跳转;ChallengeResult 也可能跳转到登录页。本项目配置了登录重定向。因此,以前将所有权限不足一概写成“直接返回 403”的描述,需要补上认证处理器这一层。

业务返回 Fail 是正常拒绝分支。数据库错误、依赖异常、未知权限编码引起的异常等,没有在当前 Checker 中统一捕获成这些失败结果,应由宿主异常处理机制处理。

15. 从 Demo 到实际业务,还要分清哪些边界

这次练习已经串起了 Attribute、Model Binding、Action Filter、DI 和自定义业务代码,但仍有以下与现有实现直接相关的边界:

  • MVC Controller 上的特性只保护经过该 Controller 的请求,不会自动保护同名 AppService 或动态 API。动态 API 的授权要在自己的入口配置。
  • Filter 检查三项验证数据,没有统一处理整个 ModelState,其他业务参数的有效性仍需由对应流程保证。
  • 直接调用 PasswordHasher 不会自动完成完整登录流程,例如失败次数累计、账户锁定处理或二次验证。当前只检查了用户启用状态和密码结果等已编写规则。
  • AllowGet 只检查 HTTP 方法,没有限制 POST 的凭据来源,也没有做全局日志脱敏或响应缓存控制。
  • Repository 和 UserManager 仍依赖 ABP 的 UnitOfWork、租户上下文和数据过滤器;从游客传入 UserId 不等于已经正确确定租户。跨租户场景尚未完成验证。

这些边界解释了为什么“写了一个特性”不等于已经实现完整的认证系统。这个 Demo 的学习重点,是把每个环节的职责放在明确的位置,方便后续替换自己的规则。

16. 已完成的验证与后续实验记录

实施阶段运行过以下构建命令:

dotnet build src/EDU.SIS.Web.Mvc/EDU.SIS.Web.Mvc.csproj --no-restore --nologo

加入 EmployeeInfo 查询和手机号日志后的那次构建记录为 0 个错误、14 个警告。这说明当时的代码通过编译,不能据此认定真实请求、DI 解析、数据库查询或日志写入已经全部通过。

本次整理只核对代码并编写博客,没有重新构建、部署或使用真实用户密码调用接口。以下保留为待执行的实验,不作为已通过结果:

待验证场景 应观察的行为
游客 GET,显式允许 GET,三项数据及权限正确 通过验证,继续执行 Action。
POST 表单,DTO 参数完整 完成参数绑定并进入验证。
JSON POST 使用当前无 FromBody 的声明 检查绑定差异,不能沿用“DTO 自动接收 JSON”的假设。
缺少参数、UserId 非法或 DTO 重名 验证失败,Action 不执行。
EmployeeInfo 不存在、密码错误、权限不足 对应失败分支,不执行手机号日志。
完整验证通过 确认日志 Provider 接收到字段,验证结果不包含 Password。
连续请求同一 Action 每次重新进入验证,不复用上一次 HttpContext.Items。
Controller 级标注及额外业务参数 所有 Action 按约定取值;业务参数继续传给 Action。

回到最初的问题,现在可以解释一条标注背后的执行关系:Attribute 声明配置,MVC 创建并调用 Filter,Filter 取得请求参数,业务服务作出验证决定,最后才决定是否执行 Action。 后续修改用户来源、员工对应关系或密码规则时,主要关注 CheckAsync;修改参数读取方式时,关注 Filter;修改使用方可配置的选项时,关注 Attribute 与 Options。

posted on 2026-09-23 09:20  醉驾的猫  阅读(7)  评论(0)    收藏  举报

导航