.net core 认证
认证、登录与注销
认证是一个旨在确定请求访问者真实身份的过程,与认证相关的还有其他两个基本操作——登录和注销。要真正理解认证、登录和注销这 3个核心操作的本质,就需要对 ASP.NET Core采用的基于“票据”的认证机制有基本的了解。
认证票据
ASP.NET Core应用的认证实现在一个名为AuthenticationMiddleware的中间件中,该中间件在处理分发给它的请求时会按照指定的认证方案(Authentication Scheme)从请求中提取能够验证用户真实身份的数据,我们一般将该数据称为安全令牌(Security Token)。ASP.NET Core 应用下的安全令牌被称为认证票据(Authentication Ticket),所以 ASP.NET Core 应用采用基于票据的认证方式。
3种针对认证票据的操作,即认证票据的颁发、检验和撤销。我们将这 3 个操作所涉及的 3 种角色称为票据颁发者(Ticket Issuer)、验证者(Authenticator)和撤销者(Ticket Revoker),在大部分场景下这 3种角色由同一个主体来扮演。
颁发认证票据的过程就是登录(Sign In)操作。一般来说,用户试图通过登录应用以获取认证票据的时候需要提供可用来证明自身身份的用户凭证(User Credential),最常见的用户凭证类型是“用户名+密码”。认证方在确定对方真实身份之后,会颁发一个认证票据,该票据携带着与该用户有关的身份、权限及其他相关的信息。
一旦拥有了由认证方颁发的认证票据,我们就可以按照双方协商的方式(如通过 Cookie或者报头)在请求中携带该认证票据,并以此票据声明的身份执行目标操作或者访问目标资源。认证票据一般都具有时效性,一旦过期将变得无效。我们有的时候甚至希望在过期之前就让认证票据无效,以免别人使用它冒用自己的身份与应用进行交互,这就是注销(Sign Out)操作。
ASP.NET Core 的认证系统旨在构建一个标准的模型,用来完成针对请求的认证以及与之相关的登录和注销操作。
如下面的代码片段所示,我们调用IApplicationBuilder接口的UseAuthentication扩展方法就是为了注册用来实现认证的 AuthenticationMiddleware 中间件。该中间件的依赖服务是通过调用IServiceCollection 接口的 AddAuthentication 扩展方法注册的。在注册这些基础服务时,我们还设置了默认采用的认证方案,静态类型 CookieAuthenticationDefaults的 AuthenticationScheme属性返回的就是Cookie认证方案的默认方案名称。
ASP.NET Core 提供了一个极具扩展性的认证模型,我们可以利用它支持多种认证方案,对认证方案的注册是通过AddAuthentication方法返回的一个AuthenticationBuilder对象来实现的。在上面提供的代码片段中,我们调用AuthenticationBuilder对象的AddCookie扩展方法完成了针对Cookie认证方案的注册。演示实例的主页是通过如下所示的 RenderHomePageAsync 方法来呈现的。由于我们要求浏览主页必须是经过认证的用户,所以该方法会利用 HttpContext 上下文的 User 属性返回的ClaimsPrincipal对象判断当前请求是否经过认证。对于经过认证的请求,我们会响应一个简单的HTML文档,并在其中显示用户名和一个注销链接。
对于匿名请求,我们希望应用能够自动重定向到登录路径。从如上所示的代码片段可以看出,我们仅仅调用当前HttpContext上下文的ChallengeAsync扩展方法就完成了针对登录路径的重定向。前面提及,注册的登录和注销路径是基于 Cookie的认证方案采用的默认路径,所以调用 ChallengeAsync方法时根本不需要指定重定向路径。
如下所示的代码片段是用于处理登录请求的 SignInAsync方法的定义,而 RenderLoginPage Async方法用来呈现登录页面。如下面的代码片段所示,对于GET请求,SignInAsync方法会直接调用 RenderLoginPageAsync方法来呈现登录界面。对于 POST请求,我们会从提交的表单中提取用户名和密码,并对其实施验证。如果提供的用户名与密码一致,我们会根据用户名创建一个代表身份的GenericIdentity对象,并利用它创建一个代表登录用户的ClaimsPrincipal对象,RenderHomeAsync方法正是利用该对象来检验当前用户是否是经过认证的。有了 Claims Principal对象,我们只需要将它作为参数调用 HttpContext上下文的 SignInAsync扩展方法即可完成登录,该方法最终会自动重定向到初始方法的路径,也就是我们的主页。
一个细节,调用 HttpContext上下文的 ChallengeAsync方法会将当前路径(主页路径“/”,经过编码后为“%2F”)存储在一个名为 ReturnUrl的查询字符串中,SignInAsync方法正是利用它实现对初始路径的重定向的。
既然登录可以通过调用当前 HttpContext上下文的 SignInAsync扩展方法来完成,那么注销操作对应的自然就是 SignOutAsync扩展方法。如下面的代码片段所示,我们定义在 Program中的 SignOutAsync扩展方法正是调用这个方法来注销当前登录状态的。我们在完成注销之后将应用重定向到主页。
1 using ConsoleApp7; 2 using Microsoft.AspNetCore.Authentication; 3 using Microsoft.AspNetCore.Authentication.Cookies; 4 using Microsoft.AspNetCore.Builder; 5 using Microsoft.AspNetCore.Hosting; 6 using Microsoft.Extensions.Caching.Distributed; 7 using Microsoft.Extensions.Caching.Memory; 8 using Microsoft.Extensions.Caching.Redis; 9 using Microsoft.Extensions.FileProviders; 10 using Microsoft.Extensions.FileSystemGlobbing; 11 using Microsoft.Extensions.Options; 12 using System.Diagnostics; 13 using System.Net; 14 using System.Security.Claims; 15 using System.Security.Principal; 16 using System.Text; 17 Host.CreateDefaultBuilder().ConfigureWebHostDefaults(webHost => 18 { 19 webHost.ConfigureServices(services => { }); 20 webHost.Configure(config => { }); 21 webHost.UseStartup<StartUp>(); 22 }).Build().Run(); 23 24 public class StartUp 25 { 26 public void ConfigureServices(IServiceCollection services) 27 { 28 //注册路由 29 services.AddRouting(); 30 //注册认证 31 services.AddAuthentication(configureOptions => 32 { 33 //设置默认认证方案 34 configureOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 35 }).AddCookie(configureOptions => 36 { 37 //默认是/Account/Login 可以自定义配置 38 configureOptions.LoginPath = "/baidu/Login"; 39 configureOptions.LogoutPath = "/baidu/Logout"; 40 }); 41 } 42 public void Configure(IApplicationBuilder builder) 43 { 44 //使用认证 45 builder.UseAuthentication(); 46 //使用路由 47 builder.UseRouting(); 48 //使用终结点 49 builder.UseEndpoints(endPoints => 50 { 51 endPoints.Map("/", ServerSign.RenderHomeAsync); 52 //默认是 Account 可以自由设置 baidu 53 endPoints.Map("/baidu/Login", ServerSign.SignIn); 54 endPoints.Map("/baidu/LoginOut", ServerSign.SignOut); 55 }); 56 } 57 } 58 59 public class ServerSign 60 { 61 public static async Task RenderHomeAsync(HttpContext httpContext) 62 { 63 if (httpContext?.User?.Identity?.IsAuthenticated == true) 64 { 65 httpContext.Response.ContentType = "text/html;charset=utf-8"; 66 string html = "<html><body>"; 67 html += "<p>认证通过</p>"; 68 html += "<a href='/baidu/LoginOut'>登出</p>"; 69 html += "</body></html>"; 70 await httpContext.Response.WriteAsync(html); 71 } 72 else 73 { 74 // 默认认证未通过跳转 75 await httpContext.ChallengeAsync(); 76 } 77 } 78 79 public static async void SignIn(HttpContext httpContext) 80 { 81 //get请求 82 if (httpContext.Request.Method.ToLower() == "get") 83 { 84 httpContext.Response.ContentType = "text/html;charset=utf-8"; 85 string html = "<html><body>"; 86 html += "<p>请登陆</p>"; 87 html += "<form method='post'> <p><label>用户名称:</label><input type='text' name='name' id ='name'></p>"; 88 html += "<input type='hidden'></input>"; 89 html += "<p><label>密码:</label><input type='password' name='password' id ='password'></p>"; 90 html += "<input type='submit'>登陆 </input> </form>"; 91 html += "</body></html>"; 92 await httpContext.Response.WriteAsync(html); 93 } 94 else 95 { 96 //post请求 97 var name = httpContext.Request.Form["name"]; 98 var password = httpContext.Request.Form["password"]; 99 if (name.Equals("1") && password.Equals("1")) 100 { 101 var identity = new GenericIdentity(name, "password"); 102 await httpContext.SignInAsync(new ClaimsPrincipal(identity)); 103 } 104 else 105 { 106 await RenderHomeAsync(httpContext); 107 } 108 } 109 } 110 public static async void SignOut(HttpContext httpContext) 111 { 112 //httpContext.Response.ContentType = "text/plain;charset=utf-8"; 113 //httpContext.Response.WriteAsync("登出"); 114 await httpContext.SignOutAsync(); 115 httpContext.Response.Redirect("/"); 116 } 117 }
测试结果
登陆成功跳转
![](https://img2023.cnblogs.com/blog/424590/202302/424590-20230221160249613-1676377565.png)
登出 跳转
登出跳转
CookieAuthenticationDefaults 源码
47 // Licensed to the .NET Foundation under one or more agreements.
48 // The .NET Foundation licenses this file to you under the MIT license.
49
50 using Microsoft.AspNetCore.Http;
51
52 namespace Microsoft.AspNetCore.Authentication.Cookies;
53
54 /// <summary>
55 /// Default values related to cookie-based authentication handler
56 /// </summary>
57 public static class CookieAuthenticationDefaults
58 {
59 /// <summary>
60 /// The default value used for CookieAuthenticationOptions.AuthenticationScheme
61 /// </summary>
62 public const string AuthenticationScheme = "Cookies";
63
64 /// <summary>
65 /// The prefix used to provide a default CookieAuthenticationOptions.CookieName
66 /// </summary>
67 public static readonly string CookiePrefix = ".AspNetCore.";
68
69 /// <summary>
70 /// The default value used by CookieAuthenticationMiddleware for the
71 /// CookieAuthenticationOptions.LoginPath
72 /// </summary>
73 public static readonly PathString LoginPath = new PathString("/Account/Login");
74
75 /// <summary>
76 /// The default value used by CookieAuthenticationMiddleware for the
77 /// CookieAuthenticationOptions.LogoutPath
78 /// </summary>
79 public static readonly PathString LogoutPath = new PathString("/Account/Logout");
80
81 /// <summary>
82 /// The default value used by CookieAuthenticationMiddleware for the
83 /// CookieAuthenticationOptions.AccessDeniedPath
84 /// </summary>
85 public static readonly PathString AccessDeniedPath = new PathString("/Account/AccessDenied");
86
87 /// <summary>
88 /// The default value of the CookieAuthenticationOptions.ReturnUrlParameter
89 /// </summary>
90 public static readonly string ReturnUrlParameter = "ReturnUrl";
91 }
UseAuthentication 源码
75 // Licensed to the .NET Foundation under one or more agreements.
76 // The .NET Foundation licenses this file to you under the MIT license.
77
78 using Microsoft.AspNetCore.Http;
79 using Microsoft.AspNetCore.Http.Features.Authentication;
80 using Microsoft.Extensions.DependencyInjection;
81
82 namespace Microsoft.AspNetCore.Authentication;
83
84 /// <summary>
85 /// Middleware that performs authentication.
86 /// </summary>
87 public class AuthenticationMiddleware
88 {
89 private readonly RequestDelegate _next;
90
91 /// <summary>
92 /// Initializes a new instance of <see cref="AuthenticationMiddleware"/>.
93 /// </summary>
94 /// <param name="next">The next item in the middleware pipeline.</param>
95 /// <param name="schemes">The <see cref="IAuthenticationSchemeProvider"/>.</param>
96 public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
97 {
98 ArgumentNullException.ThrowIfNull(next);
99 ArgumentNullException.ThrowIfNull(schemes);
100
101 _next = next;
102 Schemes = schemes;
103 }
104
105 /// <summary>
106 /// Gets or sets the <see cref="IAuthenticationSchemeProvider"/>.
107 /// </summary>
108 public IAuthenticationSchemeProvider Schemes { get; set; }
109
110 /// <summary>
111 /// Invokes the middleware performing authentication.
112 /// </summary>
113 /// <param name="context">The <see cref="HttpContext"/>.</param>
114 public async Task Invoke(HttpContext context)
115 {
116 context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
117 {
118 OriginalPath = context.Request.Path,
119 OriginalPathBase = context.Request.PathBase
120 });
121
122 // Give any IAuthenticationRequestHandler schemes a chance to handle the request
123 var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
124 foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
125 {
126 var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
127 if (handler != null && await handler.HandleRequestAsync())
128 {
129 return;
130 }
131 }
132
133 var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
134 if (defaultAuthenticate != null)
135 {
136 var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
137 if (result?.Principal != null)
138 {
139 context.User = result.Principal;
140 }
141 if (result?.Succeeded ?? false)
142 {
143 var authFeatures = new AuthenticationFeatures(result);
144 context.Features.Set<IHttpAuthenticationFeature>(authFeatures);
145 context.Features.Set<IAuthenticateResultFeature>(authFeatures);
146 }
147 }
148
149 await _next(context);
150 }
151 }
CookieAuthenticationOptions 源码
1 File: CookieAuthenticationOptions.cs
2 Web Access
3 Project: src\src\Security\Authentication\Cookies\src\Microsoft.AspNetCore.Authentication.Cookies.csproj (Microsoft.AspNetCore.Authentication.Cookies)
141 // Licensed to the .NET Foundation under one or more agreements.
142 // The .NET Foundation licenses this file to you under the MIT license.
143
144 using Microsoft.AspNetCore.DataProtection;
145 using Microsoft.AspNetCore.Http;
146
147 namespace Microsoft.AspNetCore.Authentication.Cookies;
148
149 /// <summary>
150 /// Configuration options for <see cref="CookieAuthenticationOptions"/>.
151 /// </summary>
152 public class CookieAuthenticationOptions : AuthenticationSchemeOptions
153 {
154 private CookieBuilder _cookieBuilder = new RequestPathBaseCookieBuilder
155 {
156 // the default name is configured in PostConfigureCookieAuthenticationOptions
157
158 // To support OAuth authentication, a lax mode is required, see https://github.com/aspnet/Security/issues/1231.
159 SameSite = SameSiteMode.Lax,
160 HttpOnly = true,
161 SecurePolicy = CookieSecurePolicy.SameAsRequest,
162 IsEssential = true,
163 };
164
165 /// <summary>
166 /// Create an instance of the options initialized with the default values
167 /// </summary>
168 public CookieAuthenticationOptions()
169 {
170 ExpireTimeSpan = TimeSpan.FromDays(14);
171 ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
172 SlidingExpiration = true;
173 Events = new CookieAuthenticationEvents();
174 }
175
176 /// <summary>
177 /// Determines the settings used to create the cookie.
178 /// </summary>
179 /// <remarks>
180 /// <list type="bullet">
181 /// <item><description>The default value for cookie <see cref="CookieBuilder.Name"/> is <c>.AspNetCore.Cookies</c>.
182 /// This value should be changed if you change the name of the <c>AuthenticationScheme</c>, especially if your
183 /// system uses the cookie authentication handler multiple times.</description></item>
184 /// <item><description><see cref="CookieBuilder.SameSite"/> determines if the browser should allow the cookie to be attached to same-site or cross-site requests.
185 /// The default is <c>Lax</c>, which means the cookie is only allowed to be attached to cross-site requests using safe HTTP methods and same-site requests.</description></item>
186 /// <item><description><see cref="CookieBuilder.HttpOnly"/> determines if the browser should allow the cookie to be accessed by client-side JavaScript.
187 /// The default is <c>true</c>, which means the cookie will only be passed to HTTP requests and is not made available to JavaScript on the page.</description></item>
188 /// <item><description><see cref="CookieBuilder.Expiration"/> is currently ignored. Use <see cref="ExpireTimeSpan"/> to control lifetime of cookie authentication.</description></item>
189 /// <item><description><see cref="CookieBuilder.SecurePolicy"/> defaults to <see cref="CookieSecurePolicy.SameAsRequest"/>.</description></item>
190 /// </list>
191 /// </remarks>
192 public CookieBuilder Cookie
193 {
194 get => _cookieBuilder;
195 set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
196 }
197
198 /// <summary>
199 /// If set this will be used by the CookieAuthenticationHandler for data protection.
200 /// </summary>
201 public IDataProtectionProvider? DataProtectionProvider { get; set; }
202
203 /// <summary>
204 /// The SlidingExpiration is set to true to instruct the handler to re-issue a new cookie with a new
205 /// expiration time any time it processes a request which is more than halfway through the expiration window.
206 /// </summary>
207 public bool SlidingExpiration { get; set; }
208
209 /// <summary>
210 /// The LoginPath property is used by the handler for the redirection target when handling ChallengeAsync.
211 /// The current url which is added to the LoginPath as a query string parameter named by the ReturnUrlParameter.
212 /// Once a request to the LoginPath grants a new SignIn identity, the ReturnUrlParameter value is used to redirect
213 /// the browser back to the original url.
214 /// </summary>
215 public PathString LoginPath { get; set; }
216
217 /// <summary>
218 /// If the LogoutPath is provided the handler then a request to that path will redirect based on the ReturnUrlParameter.
219 /// </summary>
220 public PathString LogoutPath { get; set; }
221
222 /// <summary>
223 /// The AccessDeniedPath property is used by the handler for the redirection target when handling ForbidAsync.
224 /// </summary>
225 public PathString AccessDeniedPath { get; set; }
226
227 /// <summary>
228 /// The ReturnUrlParameter determines the name of the query string parameter which is appended by the handler
229 /// during a Challenge. This is also the query string parameter looked for when a request arrives on the login
230 /// path or logout path, in order to return to the original url after the action is performed.
231 /// </summary>
232 public string ReturnUrlParameter { get; set; }
233
234 /// <summary>
235 /// The Provider may be assigned to an instance of an object created by the application at startup time. The handler
236 /// calls methods on the provider which give the application control at certain points where processing is occurring.
237 /// If it is not provided a default instance is supplied which does nothing when the methods are called.
238 /// </summary>
239 public new CookieAuthenticationEvents Events
240 {
241 get => (CookieAuthenticationEvents)base.Events!;
242 set => base.Events = value;
243 }
244
245 /// <summary>
246 /// The TicketDataFormat is used to protect and unprotect the identity and other properties which are stored in the
247 /// cookie value. If not provided one will be created using <see cref="DataProtectionProvider"/>.
248 /// </summary>
249 public ISecureDataFormat<AuthenticationTicket> TicketDataFormat { get; set; } = default!;
250
251 /// <summary>
252 /// The component used to get cookies from the request or set them on the response.
253 ///
254 /// ChunkingCookieManager will be used by default.
255 /// </summary>
256 public ICookieManager CookieManager { get; set; } = default!;
257
258 /// <summary>
259 /// An optional container in which to store the identity across requests. When used, only a session identifier is sent
260 /// to the client. This can be used to mitigate potential problems with very large identities.
261 /// </summary>
262 public ITicketStore? SessionStore { get; set; }
263
264 /// <summary>
265 /// <para>
266 /// Controls how much time the authentication ticket stored in the cookie will remain valid from the point it is created
267 /// The expiration information is stored in the protected cookie ticket. Because of that an expired cookie will be ignored
268 /// even if it is passed to the server after the browser should have purged it.
269 /// </para>
270 /// <para>
271 /// This is separate from the value of <see cref="CookieOptions.Expires"/>, which specifies
272 /// how long the browser will keep the cookie.
273 /// </para>
274 /// </summary>
275 public TimeSpan ExpireTimeSpan { get; set; }
276 }
277 Document OutlineProject ExplorerNamespace Explorer