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、处理结果并决定是否放行。 |
测试入口另有两个文件:
- OrderEntryController.cs:包含普通参数 GET 和 DTO POST 两个测试 Action。
- GetCurrentEmailInputDto.cs:POST 示例的输入 DTO。
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 参数,随后进入以下流程:
实际业务校验采用提前返回:任一步失败,后续检查及手机号日志都不会执行。Checker 返回成功之后,Filter 保存的是 YOLinkPermissionResult,不是包含 Password 的输入上下文。
8. 参数解析支持什么
每个验证字段独立查找,当前顺序是:
- 在
ActionArguments中按名称查找,忽略大小写。 - 找到普通参数后使用其值;值为空时失败,不再回退到 DTO。
- 未找到普通参数时,遍历其他参数对象的直接公共可读属性。
- DTO 属性只能匹配一处;找不到、找到多处或值为空时拒绝。
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。
浙公网安备 33010602011771号