ARTICLE · 1206382
从源码拆穿 ASP.NET Core 鉴权授权的底裤
为什么 AddAuthentication + AddCookie 就能用?从鉴权实质、自定义 UrlToken、四天王核心对象到三层扩展,一次拆透 ASP.NET Core 鉴权源码。
01别再只会 AddAuthentication + AddCookie!我从源码拆穿 ASP.NET Core 鉴权授权的底裤
一条请求,穿过两道门,鉴权与授权到底拦住了什么。
02开头:三行代码背后的十万个为什么
问一个扎心的问题:你用 ASP.NET Core 做鉴权授权,是不是永远就这三板斧—— `csharp builder.Services.AddAuthentication().AddCookie(); app.UseAuthentication(); app.UseAuthorization(); [Authorize] `
能用,能跑,别人问一句“为什么这样写”,你大概率只能回一句“模板就是这样”。至于它到底干了什么、请求进来之后是哪个对象在解析你的 Cookie、[Authorize] 又是怎么把请求拦下来的——全是黑盒。
这套东西恰恰是整个框架里封装最狠、绕得最深的一块:一不留神你就把它用成了“魔法”,出了问题也只能瞎试。今天这篇文章,就带你把这层魔法扒干净。
我们不空谈概念,直接拿一套真实可跑的 Demo当手术台,从本质讲到实操,再从设计讲到源码,最后落到三个层次的扩展。读完你能得到两样东西:一条请求进来,鉴权授权到底经历了什么;以及以后遇到“要加一种登录方式”的需求,你该怎么下手。
先说清楚这篇文章的四段主线: 1. 理解鉴权授权:详细流程实操 + 总结 2. 分析鉴权设计:扩展自定义鉴权方式 3. 鉴权源码全流程:多个核心对象的底层理解 4. 鉴权三层扩展:多 Scheme 的实践应用
0301 鉴权、授权:同一个古老问题,被拆成了两个动作
先回到最原始的起点。HTTP 是无状态协议——每次请求对服务器来说都是“第一次见面”,它根本不记得你。可产品偏偏需要“记住你是张三,而且张三能访问这个页面”。于是人类发明了一套从Cookie + Session到Token的演进史。
但不管你用哪种凭证方案,有一段内容永远不变,永远是三步: 1. 请求服务端,获取凭证(登录) 2. 客户端再次请求,带上凭证 3. 服务端识别凭证,判断是否允许访问
看清楚了吗?鉴权授权的本质,其实就落在第 3 步:解决“用户识别 + 判断授权”的问题。而这个问题,被框架拆成了两个动作:
鉴权(Authentication):凭证的识别与解析——有没有登录?凭证有没有过期?你是张三还是李四?
授权(Authorization):基于解析结果做权限检测——张三/李四,到底有没有权限访问这个资源?
这两个词太容易混,我送你一句记住它俩的关系:鉴权是“验明正身”,授权是“验明之后才谈资格”。而且注意,授权阶段其实也会偷偷再调一次鉴权——它俩是通过 context.User 这个中间人通信的。这一点,等你看完源码就会彻底明白。
下面先带着这份基本理解,去实操里走一遍。
0402 快捷实操:把完整鉴权授权流程先跑一遍
实操的东西“过程有点长、东西有点多”,所以我只带大家完整走一遍,走完你就有了直观画面。
052.1 先搭好三样东西
第一样,注册鉴权/授权服务。鉴权这块用 Cookie 方式(LoginPath 是未登录时跳转的登录页,AccessDeniedPath 是“有凭证但没权限”时跳转的 403 页):
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>{options.LoginPath = "/Home/Index"; // 未登录 → 跳登录页options.AccessDeniedPath = "/Home/Privacy"; // 有凭证无权限 → 403});// 授权:AddMVC 里其实已经带了 AddAuthorization,// 想加自定义 Policy 再单独配置即可
第二样,把两个中间件挂到管道上(位置必须在 UseRouting 之后、MapControllerRoute 之前):
app.UseAuthentication(); // 保证请求来了都做一次凭证解析app.UseAuthorization(); // 保证请求来了都做一次授权检测
第三样,写一个 登录方法。登录的本质,是造一个 ClaimsPrincipal(一堆声明 Claims 拼起来的“用户”),然后调 SignInAsync 把它交给框架,框架负责写入 Cookie:
[HttpPost] // /Authentication/Login?name=Eleven&password=123456publicasync Task<IActionResult> Login(string name, string password){if ("YueXuan".Equals(name, StringComparison.CurrentCultureIgnoreCase)&& password.Equals("123456")){var claimIdentity = new ClaimsIdentity("Custom"); // 声明身份claimIdentity.AddClaim(new Claim(ClaimTypes.Name, name));claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "777@qq.com"));claimIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));claimIdentity.AddClaim(new Claim(ClaimTypes.Country, "Chinese"));await base.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,new ClaimsPrincipal(claimIdentity),new AuthenticationProperties { ExpiresUtc = DateTime.UtcNow.AddMinutes(30) });return new JsonResult(new { Result = true, Message = "登录成功" });}return new JsonResult(new { Result = false, Message = "登录失败" });}
这一串看着多,其实只干了两件事:把“我是谁、我有什么角色”放进声明里,再让框架把这些声明序列化进 Cookie。后续每次请求,框架就把 Cookie 反序列化回 context.User,你就又能读到这些声明了。
062.2 用 [Authorize] 家族做授权检测
授权检测的用法,本质就是一套特性标记,告诉框架“这个方法需要哪种授权”:
[Authorize] // 只需要登录过(鉴权通过即可)public IActionResult InfoWithAuthorize() { ... }[Authorize(Roles = "Admin")] // 登录 + 角色必须是 Adminpublic IActionResult InfoWithRolesAdmin() { ... }[Authorize(Policy = "MutiPolicy", AuthenticationSchemes = "Cookies")]public IActionResult InfoWithPolicy() { ... } // 登录 + 满足一个复杂策略[AllowAnonymous] // 免登录直接访问public IActionResult InfoWithAllowAnonymous() { ... }
其中 MutiPolicy 是在配置里用 AddPolicy 拼出来的一条“复合规则”,它能同时要求角色、用户名、声明、甚至是自定义断言(比如“Email 必须是某个域名”):
builder.Services.AddAuthorization(options =>{options.AddPolicy("MutiPolicy", policyBuilder =>{policyBuilder.RequireRole("Admin") // 必须 Admin 角色.RequireUserName("Yuexuan") // 必须叫 Yuexuan.RequireClaim(ClaimTypes.Country) // 必须有 Country 声明.RequireAssertion(context => // 自定义断言,还能校验 IP 等context.User.HasClaim(c => c.Type == ClaimTypes.Email)&& context.User.Claims.First(c => c.Type == ClaimTypes.Email).Value.Equals("777@qq.com")).AddRequirements(new SingleEmailRequirement("@qq.com")); // 自定义 Requirement});});
这里有个很关键的细节你以后一定会碰到:[Authorize(Policy = "MutiPolicy", AuthenticationSchemes = "Cookies")] 里那个 AuthenticationSchemes,就是“授权时用哪个/哪些 Scheme 去取用户信息”。这一点,在第四部分讲多 Scheme 时是重头戏。
072.3 主动鉴权、主动授权:把中间件的事自己干一遍
除了靠特性自动拦截,框架也允许你在代码里“手动触发”。这两个接口是理解鉴权授权的钥匙:
// 主动鉴权:手动调 AuthenticateAsync 去解析凭证,拿到 Principalvar result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);if (result?.Principal != null) HttpContext.User = result.Principal;// 主动授权:先鉴权,再判断,该挑战就挑战,该拒绝就拒绝if (!user.Identity.IsAuthenticated)await HttpContext.ChallengeAsync(scheme); // 未登录 → 401/跳登录else if (!user.Identity.Name.Equals("Yuexuan"))await HttpContext.ForbidAsync(scheme); // 无权限 → 403
是不是更清楚了?ChallengeAsync(未登录)和 ForbidAsync(无权限)这两个动作,其实就是 [Authorize] 拦下请求后框架替你调用的东西。你手动调,就等于把“魔法”拆开看了。
082.4 一张表记住状态码与动作
走完这一遍你会发现:配置一下就能生效,封装的确实很厉害。但反过来也说明,你完全不知道它内部发生了什么。下面,我们从“设计”层面开始深入。
0903 深入鉴权设计:鉴权到底要干哪几件事
把鉴权细化、拆开看,你会发现无论 Cookie、JWT 还是别的,本质上都是在回答同一个问题:“怎么从凭证里认出张三?”而要回答它,必须处理五件事: 1. 凭证位置:凭证是怎么传递的?Cookie 放 Header 里,JWT 也放 Header,但读取方式不同。 2. 凭证格式:凭证是什么格式?加密、序列化、还是 JWT 的 Base64 三段式? 3. 信息有效性:过期时间、签名校验、token 是否有效——这些怎么判断? 4. 鉴权信息传递:解析出来的用户信息要保存下来给后面用,就放进 context.User。 5. 特殊情况处理:没登录、没权限时,该怎么跳转、该返回什么状态码?
这五件事,就是“一次鉴权”的全部职责。理解了它,你就能理解为什么框架要设计成“Scheme + Handler”——因为每种凭证方式,无非就是把这五件事的实现换一套而已。
1004 自定义鉴权:自己写一个 UrlToken 鉴权方式
光看 Cookie 鉴权,你还是看不清“凭证在哪、格式是啥”是被谁控制的。干脆,我们手写一种全新的鉴权方式——UrlToken 鉴权:凭证放在 URL 的查询参数里,格式是 eleven-123456。写完你会发现,自定义鉴权根本没有想象中那么玄。
114.1 核心:实现 IAuthenticationHandler
自定义鉴权的第一步,是实现 IAuthenticationHandler。这个接口就是“鉴权动作”的全部契约,它有四个方法:
public class UrlTokenAuthenticationHandler: IAuthenticationHandler, // 鉴权核心:Authenticate / Challenge / ForbidIAuthenticationSignInHandler, // 登录:SignInAsyncIAuthenticationSignOutHandler // 登出:SignOutAsync{private AuthenticationScheme _AuthenticationScheme;private HttpContext _HttpContext;// ① 初始化:Provider 把 Scheme 和 HttpContext 注入进来(像方法注入)public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context){_AuthenticationScheme = scheme;_HttpContext = context;return Task.CompletedTask;}// ② 核心鉴权方法:解析用户信息public Task<AuthenticateResult> AuthenticateAsync(){string userInfo = _HttpContext.Request.Query["UrlToken"]; // 凭证从哪读if (string.IsNullOrWhiteSpace(userInfo))return Task.FromResult(AuthenticateResult.NoResult()); // 没带 → Challengeif ("eleven-123456".Equals(userInfo)) // 校验规则,可提到 Options{var claimIdentity = new ClaimsIdentity("Custom");claimIdentity.AddClaim(new Claim(ClaimTypes.Name, userInfo));claimIdentity.AddClaim(new Claim(ClaimTypes.Role, "User"));claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "123@126.com"));var principal = new ClaimsPrincipal(claimIdentity);// 成功后,把用户信息封装成 Ticket 往后传return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, null, _AuthenticationScheme.Name)));}return Task.FromResult(AuthenticateResult.Fail("UrlToken is wrong")); // → Forbid}// ③ 未登录:跳登录页public Task ChallengeAsync(AuthenticationProperties properties){_HttpContext.Response.Redirect("/Home/Index");return Task.CompletedTask;}// ④ 无权限:返回 403public Task ForbidAsync(AuthenticationProperties properties){_HttpContext.Response.StatusCode = 403;return Task.CompletedTask;}}
看到没,自定义鉴权的关键就一句话:实现 AuthenticateAsync,告诉框架“凭证从哪读、怎么校验、成功后拼出什么 Principal”;再实现 Challenge/Forbid,告诉框架“没登录/没权限时怎么办”。
顺带解释一个常见困惑:为什么 SignInAsync / SignOutAsync 是独立接口而不是塞进 IAuthenticationHandler?因为现代架构通常有统一的认证中心负责发证和销毁,而其它服务只负责验证证书——它们根本用不到登录登出。把 SignIn 单独拆出来,就是为了让“只验证、不签发”的服务不必实现它。
124.2 注册:AddScheme,告诉框架“怎么解析”
builder.Services.AddAuthentication(options =>{// 用一个固定名字(UrlTokenScheme) 绑定到我们自定义的 Handler 类型options.AddScheme<UrlTokenAuthenticationHandler>(UrlTokenAuthenticationDefaults.AuthenticationScheme, "UrlTokenScheme-Demo");// 关键:把默认 Scheme 全指向 UrlTokenSchemeoptions.DefaultAuthenticateScheme = UrlTokenAuthenticationDefaults.AuthenticationScheme;options.DefaultChallengeScheme = UrlTokenAuthenticationDefaults.AuthenticationScheme;options.DefaultSignInScheme = UrlTokenAuthenticationDefaults.AuthenticationScheme;options.DefaultForbidScheme = UrlTokenAuthenticationDefaults.AuthenticationScheme;options.DefaultSignOutScheme = UrlTokenAuthenticationDefaults.AuthenticationScheme;});
这套 AddAuthentication + AddScheme,本质上就是“告诉框架:当有人请求 'UrlTokenScheme' 这个 Scheme 时,去实例化 UrlTokenAuthenticationHandler 来处理”。剩下的,框架全包了——请求一到,自动调你的 Handler 完成鉴权。
134.3 验证:标记 + 带 Token 就过,不带就跳
// /Authentication/UrlToken?UrlToken=eleven-123456[Authorize] // 默认走 UrlTokenSchemepublicasync Task<IActionResult> UrlToken() { ... }// 或显式指定 Scheme// [Authorize(AuthenticationSchemes = UrlTokenAuthenticationDefaults.AuthenticationScheme)]
到了这一步,我们不光会“用”,还能“造”——自定义一套鉴权方式,全程没碰框架内部一行代码,只实现了几个接口。这说明什么?ASP.NET Core 的鉴权体系设计得非常开:Handler 是唯一扩展点,其余全部复用。
看到这里,你肯定想问了:注册时到底存了什么?请求进来又是怎么找到这个 Handler 的?这就到了重头戏——源码全流程。
1405 源码全流程:一条请求是怎么被鉴权的
实操做完了,自定义也做完了,现在我们进入核心中的核心:框架源码到底怎么跑?要直接调试微软的源码挺费劲,所以我们用一个“新套路”:把源码拷贝过来、改个名、替换掉原注册(比如把 AddAuthentication 换成自定义的 CustomAddAuthentication,中间件换成 CustomUseAuthentication),在本地项目里直接断点调试。这也是彻底搞懂框架最有效的方式。
整套鉴权,其实就两大环节:启动环节(AddAuthentication + UseAuthentication)和请求环节(一次 Invoke)。下面一步步拆。
155.1 启动一:AddAuthentication —— 注册 4 个核心对象
入口是 AddAuthentication,核心逻辑在 AddAuthenticationCore。它做的事,是把鉴权最底层的 4 个核心对象注册进 IOC:
services.TryAddScoped<IAuthenticationService, AuthenticationService>(); // 鉴权服务services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); // 声明转换(空实现)services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>(); // Handler 提供者services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>(); // Scheme 提供者// 顺带配置 AuthenticationOptions,并指定默认 DefaultScheme
先别被吓到,这 4 个对象就是全文最重要的记忆点,我们把它们记成“鉴权四天王”:
而 IClaimsTransformation(默认 NoopClaimsTransformation,空实现)是第 5 个隐藏角色,负责“解析完之后给 Principal 做一次转换”,后面讲扩展时会用到。
那 AddScheme 又是怎么回事?关键在这:
// AuthenticationOptions 里publicvoidAddScheme<TOptions, THandler>(...) where ...{// 其实只是 new 一个 AuthenticationSchemeBuilder,把 Handler 类型存进去Schemes.Add(new AuthenticationSchemeBuilder(name) { HandlerType = typeof(THandler), ... });}// 注意:此时只是"Builder",还没 Build 成 AuthenticationScheme
重点来了:AddScheme 阶段并没有真正生成 AuthenticationScheme,只是建了个 Builder 存着 HandlerType,用 Name 当 key 存进字典。AddCookie、AddJWT、我们的 AddUrlToken——本质上全一样,都是“字符串 Name 对应一个 Handler 类型”。这就是为什么你可以随意扩展新的鉴权方式。
165.2 启动二:UseAuthentication —— 中间件把 Scheme 真正 Build 出来
注册完 IOC,就该把中间件挂上管道了。挂 UseAuthentication 时会 UseMiddleware<AuthenticationMiddleware>,而这个中间件的构造函数,注入了 AuthenticationSchemeProvider:
publicAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes){_next = next;Schemes = schemes; // 注入 Scheme 提供者}// AuthenticationSchemeProvider 构造函数里:foreach (var builder in _options.Schemes) // 遍历之前 AddScheme 存的 Builder{var scheme = builder.Build(); // 终于 Build:得到 name + displayName + HandlerTypeAddScheme(scheme); // 缓存进字典}
也就是说:启动环节收尾时,Provider 把 AddScheme 存的所有 Builder 一次性 Build 成真正的 AuthenticationScheme(带 HandlerType),并缓存进字典。到这一步,“哪个 Name 对应哪个 Handler”才真正落定。
175.3 请求环节:一次 Invoke,绕一圈完成鉴权
启动搞定,请求来了。中间件的 Invoke 方法会执行四步:
publicasync Task Invoke(HttpContext context){context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature{ OriginalPath = context.Request.Path, OriginalPathBase = context.Request.PathBase });// ① 扩展点:先给 IAuthenticationRequestHandler 类型一个机会"直接处理请求"(不走后续)var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()){var handler = await handlers.GetHandlerAsync(context, scheme.Name)as IAuthenticationRequestHandler;if (handler != null && await handler.HandleRequestAsync()) return;}// ② 取默认鉴权 Schemevar defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();if (defaultAuthenticate != null){// ③ 主动鉴权:AuthenticateAsync 内部会一路绕下去var result = await context.AuthenticateAsync(defaultAuthenticate.Name);if (result?.Principal != null) context.User = result.Principal; // ④ 保存到 context.User}await _next(context);}
中间这步 context.AuthenticateAsync 是关键,它内部是一套Provider → Handler → Service的多对象协作,我们把调用链画出来: `text context.AuthenticateAsync(name) └─> AuthenticationService.AuthenticateAsync(name) ├─ ① 用 AuthenticationHandlerProvider.GetHandlerAsync(context, name) │ └─ 用 AuthenticationSchemeProvider 找到 AuthenticationScheme │ └─ 从字典缓存取 Handler;没有就实例化 + InitializeAsync(scheme, context) + 缓存 └─ ② 调 handler.AuthenticateAsync() ← 我们自定义的解析逻辑就在这 └─ ③ 成功 → IClaimsTransformation.TransformAsync(principal) 做一次转换 └─ ④ 包成 AuthenticationTicket 返回 → 中间件存进 context.User `
绕来绕去,价值就一个词:单一职责。SchemeProvider 管“有哪些方式”,SchemeProvider 管“名字对应什么 Handler”,HandlerProvider 管“造 Handler、初始化、缓存”,Service 管“串联整个流程”,Handler 管“具体的凭证解析”,Transformation 管“转换”。每个对象只干一件事,所以你想扩展任何一个环节,都只需要换掉对应的对象——这,就是它能被你“随意自定义”的根本原因。
185.4 一个执行顺序的细节
这里有个很关键的时序逻辑: 1. AddAuthentication:注册 4 核心对象 + 把 Scheme 和 Handler 注册进 Options(只存 Builder)。 2. UseAuthentication:注册中间件,注入 Provider,在 Provider 构造函数里把 Builder 全部 Build 成 Scheme 并缓存。 3. 请求来了:用默认 Scheme 的名字,按上面那条链绕一圈,完成鉴权,把用户信息存进 context.User。
到这一步,你已经看懂了 ASP.NET Core 鉴权 80% 的真相。剩下 20%,是“三个层次的扩展”——这也是面试和实战里最能拉开差距的地方。
1906 鉴权的三层扩展:Handler、Transformation、多 Scheme
看完源码,我们回到实战最关心的部分——遇到“要加一种登录方式 / 要改造用户信息 / 多种登录并存”时,怎么扩展。这正好是鉴权的三个扩展层次,一层比一层深。
206.1 第一层扩展:自定义 IAuthenticationHandler(换“凭证解析”)
就是我们第四部分写的 UrlTokenAuthenticationHandler。它解决的是“凭证来源、凭证格式、校验规则”全部自定。要加一种新的登录方式(比如“临时授权链接”“扫码”),你就实现一套新的 Handler + AddScheme 注册,其余全复用。这是最彻底的扩展,也是理解源码的入场券。
216.2 第二层扩展:自定义 IClaimsTransformation(改“用户信息”)
有时候你不想换凭证方式,只想在“解析完之后、存进 context.User 之前”,给用户信息加点料。这就是 IClaimsTransformation 的用武之地。默认是空实现(NoopClaimsTransformation,原样返回),你可以替换成自己的:
public class CustomClaimsTransformation : IClaimsTransformation{public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal){// 如果是中国人,就给声明里补一条 language=cnif (principal.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.Country))?.Value.Equals("Chinese") ?? false){principal.Claims.Append(new Claim("language", "cn"));}return Task.FromResult(principal);}}// 替换注册(替换默认的 NoopClaimsTransformation):builder.Services.Replace(ServiceDescriptor.Scoped<IClaimsTransformation,CustomClaimsTransformation>());
注意它的执行时机:在 AuthenticationService.AuthenticateAsync 里,handler 解析成功之后、返回 Ticket 之前,会调 Transform.TransformAsync(principal) 一次。最常见的落地场景是:从 DB/Redis 实时补充用户角色、权限、部门等声明——不用把大而全的信息都塞进 Cookie。
226.3 第三层扩展:多 Scheme 并存(Cookie + UrlToken 同时在线)
这是最容易踩坑、也最能体现功底的一层。程序完全可以同时支持多个 Scheme 并存——比如同一个站点,Web 端走 Cookie,接口/扫码走 UrlToken:
builder.Services.AddAuthentication(options =>{options.AddScheme<UrlTokenAuthenticationHandler>(UrlTokenAuthenticationDefaults.AuthenticationScheme,"UrlTokenScheme-Demo");// 默认 Scheme 指向 UrlTokenoptions.DefaultAuthenticateScheme = UrlTokenAuthenticationDefaults.AuthenticationScheme;options.DefaultChallengeScheme = UrlTokenAuthenticationDefaults.AuthenticationScheme;...}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>{options.LoginPath = "/Home/Index";options.AccessDeniedPath = "/Home/Privacy";}); // 又一个 Scheme:Cookie
但这里有个极易误解的点,必须讲透:鉴权中间件(UseAuthentication)只走“默认 Scheme”做一次解析,并不会把所有 Scheme 都解析一遍。那“其他 Scheme 的用户信息怎么拿到”?答案是——靠授权标记([Authorize] 上的 AuthenticationSchemes)来指定,或者靠主动鉴权挨个取:
// 方式一:显式指定用哪个 Scheme 授权[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]// [Authorize(AuthenticationSchemes = "Cookies,UrlTokenScheme")] // 多个 Scheme 逗号分隔// 方式二:主动鉴权,把多个 Scheme 的用户信息都拿出来var urlToken = await HttpContext.AuthenticateAsync(UrlTokenAuthenticationDefaults.AuthenticationScheme);var cookie = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
那么问题来了:多 Scheme 并存时,授权到底怎么算?这其实是个复杂话题,关键在“Principal 的合并”——框架会通过 SecurityHelper.MergeUserPrincipal 把多个 Scheme 解析出的身份合并成一个 ClaimsPrincipal(新身份放前面,旧的非空身份保留),这样授权检测就能同时看到两边的声明:
publicstatic ClaimsPrincipal MergeUserPrincipal(ClaimsPrincipal? existing,ClaimsPrincipal? additional){var newPrincipal = new ClaimsPrincipal();if (additional != null) newPrincipal.AddIdentities(additional.Identities); // 新身份在前if (existing != null) newPrincipal.AddIdentities(existing.Identities.Where(i => i.IsAuthenticated || i.Claims.Any())); // 旧的非空身份保留return newPrincipal;}
这正是你在 Demo 里能看到 UrlCookieByDouble、UrlCookieByUrlToken、UrlCookieByCookie 好几个 Action 的原因——同样的资源,用不同 Scheme(或 Scheme 组合)授权,得到的是不同身份来源,声明的交集/并集也就不同。理解多 Scheme,核心就是理解“默认只解析默认 Scheme,其它靠标记指定,最终按需合并 Principal”。
236.4 顺带一提:Ticket 的存放也能扩展
再深一层,连“鉴权成功后 Ticket 存哪”都能改。默认 Cookie 鉴权把 Ticket 序列化进 Cookie;你也可以实现 ITicketStore 存到内存/Redis,Cookie 里只放一个 Key。Demo 里的 MemoryCacheTicketStore 就是例子,适合“凭证太长、想减轻 Cookie 体积”或“想统一管理会话”的场景。鉴权的扩展性,就是设计出来的。
2407 写在最后:你该带走的判断框架
文章有点长,但值得。最后给你一套能直接带走的判断框架:
以后遇到鉴权授权需求,先回答三个问题:
1. 要“认人”还是“认方式”?只是用户信息加工,走第二层(IClaimsTransformation);要全新的凭证格式/来源,走第一层(自定义 IAuthenticationHandler)。
2. 要不要多种登录并存?要,就走第三层多 Scheme,记住“默认只解析默认 Scheme,其它靠 [Authorize] 的 AuthenticationSchemes 指定,按需合并 Principal”。
3. 授权规则够不够灵活?内置 Roles/Policy 不够,就自定义 Requirement + Handler(实现 IAuthorizationHandler),这是授权侧的另一个扩展世界。
回到开头那个问题:为什么 AddAuthentication + AddCookie + UseAuthentication + [Authorize] 能用?因为框架把“凭证位置、凭证格式、有效性校验、信息传递、异常处理”五件事全部封装成了可替换的对象。你看懂了这条链:AddScheme 存 Builder → 中间件 Build 成 Scheme → 请求来用默认 Scheme → HandlerProvider 造 Handler → handler.AuthenticateAsync 解析 → IClaimsTransformation 转换 → 存进 context.User——从此它对你不再是魔法。
理解鉴权,就是理解“怎么把该记的东西记清楚、该拆的职责拆干净”。框架给你留了接口,剩下的,就看你要往哪个方向扩展了。
这套体系讲完鉴权,还有更大的半边天——授权(Authorization)的策略组装、Policy 的构建、以及多 Scheme 下的授权合并。那才是“鉴权之后真正决定谁能进来”的最后一公里。想接着深入,点个关注,下篇见分晓。
THE END















































































