在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Most of the literature concerning the theme of authentication in ASP.NET Core focuses on the use of the ASP.NET Identity framework. In that context, things don’t seem to have changed much or, more precisely, all the changes that occurred in the infrastructure have been buried in the folds of the framework so that it looks nearly the same on the surface.
If you look at user authentication in ASP.NET Core outside the comfortable territory of ASP.NET Identity, you might find it quite different from what it was in past versions. ASP.NET Identity is a full-fledged, comprehensive, big framework that's overkill if all you need is to authenticate users via plain credentials from a simple database table. In this case, you'll see that the overall approach to authentication is still based on familiar concepts such as principal, login form, challenge and authorization attributes, except that the way you implement them is radically different. In this month's column, I'll explore the cookie authentication API as made available in ASP.NET Core, including the core facts of external authentication.
Foundation of ASP.NET Authentication
There are two major changes in ASP.NET Core for those coming from an ASP.NET Web Forms and ASP.NET MVC background. First, there's no longer a web.config file, meaning that configuration of the login path, cookie name and expiration is retrieved differently. Second, the IPrincipal object—the object used to model user identity — is now based on claims rather than the plain user name. To enable cookie authentication in a brand-new ASP.NET Core 1.x application, you first reference the Microsoft.AspNetCore.Authentication.Cookies package and then add the code snippet in Figure 1.
Figure 1 Registering Middleware for Cookie Authentication // This code is for ASP.NET Core 1.x public void Configure(IApplicationBuilder app) { app.UseCookieAuthentication(new CookieAuthenticationOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, AuthenticationScheme = "Cookies", CookieName = "YourAppCookieName", LoginPath = new PathString("/Account/Login"), ExpireTimeSpan = TimeSpan.FromMinutes(60), SlidingExpiration = true, ReturnUrlParameter = "original", AccessDeniedPath = new PathString("/Account/Denied") }); }
Most of the information that classic ASP.NET MVC applications stored in the <authentication> section of the web.config file are configured as middleware options. The snippet in Figure 1 comprehends canonical options you might want to choose. Figure 2 explains each in more detail. Figure 2 Cookie Authentication Options
注意上面这个表格的很多属性从ASP.NET Core 2.X开始,应该在Startup类的ConfigureServices方法中进行设置,例如下面就演示了如何在ASP.NET Core 2.X中设置Cookie认证的Cookie名字: // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { //注册Cookie认证服务 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(option => { option.Cookie.Name = "AspNetCookieAuth";//设置Cookie认证的Cookie名字 }); services.AddMvc(); }
Note that path properties are not of type string. LoginPath and AccessDeniedPath are of type PathString, which, compared to the plain String type, provides correct escaping when building a request URL.
The overall design of the user authentication workflow in ASP.NET Core gives you an unprecedented amount of flexi-bility. Every aspect of it can be customized at will. As an example, let's see how you can control the authentication work-flow being used on a per-request basis.
Dealing with Multiple Authentication Schemes
app.UseCookieAuthentication(new CookieAuthenticationOptions() { AuthenticationScheme = "Cookies", LoginPath = new PathString("/Account/Login/"), AutomaticAuthenticate = true }); app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions { AuthenticationScheme = "Bearer", AutomaticAuthenticate = true }
Be aware that there are constants to be used in place of magic strings like "Cookies" just to limit typos. In particular, the string "Cookies" can be replaced as below: AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme
Note that UseIdentityServerAuthentication isn't part of the ASP.NET Core framework but belongs to the Identity Server framework (see github.com/IdentityServer). To choose the authentication scheme on a per-request basis, you use a new attribute on the Authorize attribute that in ASP.NET MVC marks actions as subject to authentication and authorization: [Authorize(ActiveAuthenticationSchemes = "Bearer")] public class ApiController : Controller { // Your API action methods here ... } The net effect of the code snippet is that all public endpoints of the sample ApiController class are subject to the identity of the user as authenticated by the bearer token.
Modeling the User Identity
GenericPrincipal wraps up one key piece of user information—the user name—even though custom user data can be added to the authentication ticket encrypted in the cookie. Over the years, the sole user name has become too little for the needs of modern applications. The role of the user, as well as some other chunks of information, most noticeably picture and display name, appear absolutely required today, forcing every realistic application to create its own custom principal type or query user information for each and every request using the user name as the key. ClaimsPrincipal just brilliantly solves the problem.
A claim is a key/value pair that describes a property of the logged user. The list of properties is up to the application, but includes name and role at the very minimum. Claims, in other words, are the ASP.NET Core way to model identity information. Claims can be read from any source, whether databases, cloud or local storage, even hardcoded. The Claim class is as simple as this: public class Claim { public string Type { get; } public string Value { get; } // More properties ... }
The login process in ASP.NET Core passes through three classes: Claim, ClaimIdentity and ClaimsPrincipal. public Claims[] LoadClaims(User user) { var claims = new[] { new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.Role, user.Role), new Claim("Picture", user.Picture) }; return claims; }
The name of the claim is a plain descriptive name rendered as a string. However, most common claim types have been grouped as constants into the ClaimTypes class. Before you create the principal, which is required to call the authentication workflow completed, you must get hold of an identity object: var identity = new ClaimsIdentity(claims, "Password");
The first argument is self-explanatory—the list of claims associated with the identity being created. The second argument is a string that refers to the authentication scheme required to verify the identity. The string "Password" is a reminder of what will be required by the system for a user to prove her identity. The string "Password" is informational only and not a syntax element.
Another interesting aspect of the previous example is that an explicit user name is not strictly required. Any claim, regardless of the declared type, can be used to name the user. The following code snippet shows another equivalent way to have a new identity object: var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme, "Nickname", ClaimTypes.Role); The new identity has "Cookies" as the authentication scheme and Nickname is the name of the claim in the provided list to be used to provide the name of the user. Role, instead, is the name of the claim in the same list determining the role. If not specified, the last two parameters default to ClaimTypes.Name and ClaimTypes.Role. Finally, you create the principal from the identity. It's worth noting, though, that a principal may have multiple identities. If it sounds weird, think that different areas of the same application might need different information to authenticate the user in much the same way an ID is required to identify yourself at some hotel desks and an electronic key to get into the elevator. The ClaimsPrincipal class has both an Identities property (a collection) and an Identity property. The latter is only a reference to the first item in the collection.
External Authentication
app.UseTwitterAuthentication(new TwitterOptions() { AuthenticationScheme = "Twitter", SignInScheme = "Cookies", ConsumerKey = "...", ConsumerSecret = "..." });
The SignInScheme property is the identifier of the authentication middleware that will be used to persist the resulting identity. In the example, an authentication cookie will be used. To see its effects, you first add a controller method to call into Twitter: public async Task TwitterAuth() { var props = new AuthenticationProperties { RedirectUri = "/" }; await HttpContext.Authentication.ChallengeAsync("Twitter", props); }
Next, once Twitter has successfully authenticated the user, the SignInScheme property instructs the application on what to do next. A value of "Cookies" is acceptable if you want a cookie out of the claims returned by the external provider (Twitter, in the example). If you want to review and complete the information through, say, an intermediate form, then you have to break the process in two, introducing a temporary sign-in scheme. In addition to the standard cookie middleware you have the following: app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = "ExternalCookie", AutomaticAuthenticate = false }); app.UseTwitterAuthentication(new TwitterOptions { AuthenticationScheme = "Twitter", SignInScheme = "ExternalCookie" });
When the external provider returns, a temporary cookie is created using the ExternalCookie scheme. Having set the redirect path appropriately, you have a chance to inspect the principal returned by Twitter and edit it further: var props = new AuthenticationProperties RedirectUri = "/account/external" };
To complete the workflow you also need to sign in in the cookies scheme and sign out of temporary scheme (ExternalCookie): public async Task<IActionResult> External() { var principal = await HttpContext .Authentication .AuthenticateAsync("ExternalCookie"); // Edit the principal ... await HttpContext.Authentication.SignInAsync("Cookies", principal); await HttpContext.Authentication.SignOutAsync("ExternalCookie "); return View(); }
ExternalCookie, as well as cookies, are just internal identifiers and can be renamed as long as they remain consistent throughout the application.
Wrapping Up
Everything stated in this article refers to ASP.NET Core 1.x. There are a few things that will work differently in ASP.NET Core 2.0. In particular, authentication middleware is now exposed as services and must be configured on Configure-Services: services.AddCookieAuthentication(options => { Options.LoginPath = new PathString("/Account/Login"), options.AutomaticAuthenticate = true, options.AutomaticChallenge = true, options.AuthenticationScheme = "Cookies", ... });
In the Configure method of the Startup class of ASP.NET Core 2.0 applications, you just declare your intention to use authentication services without any further options: app.UseAuthentication();
Also note that the SignInAsync method you use in your code to create the authentication cookie is also exposed from the HttpContext object directly, instead of passing through an intermediate Authentication property as shown in the last code snippet for ASP.NET Core 1.x.
|
请发表评论