领先技术

使用 ASP.NET 标识进行外部身份验证

Dino Esposito

Dino EspositoVisual Studio 2013 新增了用于构建应用程序的 ASP.NET MVC 5 和 Web 窗体项目模板。这些示例模板可能会视需要选择包含基于 ASP.NET 标识的默认身份验证层。为您生成的代码中包含许多功能,但是读懂代码或许并不是一件容易的事情。我将在本文中说明如何通过基于外部 OAuth 或 OpenID 的服务器(如社交网络)进行身份验证,并介绍了您在身份验证完成且有权访问所有已下载的声明后可能需要执行的步骤。

我将从一个空的 ASP.NET MVC 5 应用程序入手,然后添加应用程序可能需要的最基本身份验证层。您会发现,新增的 ASP.NET 标识框架将整个身份验证堆栈的复杂程度和功能提升到了一个新高度,但同时又使您可以轻松、快捷地针对外部用户身份验证进行安排,而无需了解新的程序包和 API。

分离出帐户控制器

自第一版 ASP.NET MVC 起,这个表示新用户看到的框架外观的示例应用程序就提供了一个相当繁杂的控制器类来管理身份验证的各个方面。在 2014 年 3 月的专栏“ASP.NET 标识初探”(msdn.microsoft.com/magazine/dn605872) 中,我从您在 Visual Studio 中默认获取的代码入手概述了 ASP.NET 标识框架。但在本文中,我将从一个空的 ASP.NET MVC 5 项目入手,对各个部分进行逐一介绍。这样,我便能够认真遵循单一责任原则 (bit.ly/1gGjFtx)。

我将从一个空项目入手,然后添加一些 ASP.NET MVC 5 基架。接下来,我将添加两个新的控制器类:Login­Controller 和 AccountController。前者只关注登录和注销操作是通过网站专用的本地成员身份系统还是通过外部 OAuth 提供程序(如 Twitter)完成。顾名思义,AccountController 类包含主要与用户帐户管理相关的其他所有功能。为简单起见,我将暂时只包含注册新用户的功能。图 1 展示了这两个控制器类在 Visual Studio 2013 中的框架。

Skeleton of LoginController and AccountController Classes in Visual Studio 2013
图 1:Visual Studio 2013 中的 LoginController 和 AccountController 类的框架

UI 划分为 3 个分部视图:登录用户、本地登录窗体和社交登录窗体,如图 2 所示。

The Three Partial Views Used in the Authentication Stack
图 2:身份验证堆栈中使用的 3 个分部视图

IdentityController 类

图 1 显示的框架中,LoginController 和 AccountController 类均继承自 ASP.NET MVC 5 控制器基类。但是,如果您要使用 ASP.NET 标识框架,那么这可能不是一个理想的选择,因为这样会生成一些重复的代码。我建议您在 Visual Studio 2013 中创建一个临时项目,然后查看在全面的 AccountController 类中存储的默认代码。ASP.NET 标识要求您将 UserManager<TUser> 类和存储机制插入控制器中。此外,您可能还希望使用几个帮助程序方法来简化用户登录和重定向过程。您还需要一些属性来引用 UserManager 根对象,或许还需要另一个帮助程序属性来建立与基础 Open Web Interface for .NET (OWIN) 中间件的联系点。

引入中间类可能是一种不错的选择,这样就可以顺利地一次(只需一次)填充所有这种 Boilerplate 代码。我将调用这个 IdentityController 类,并按图 3 所示对其进行定义。

图 3:ASP.NET 标识身份验证的新基类

using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using AuthSocial.ViewModels.Account.Identity; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; namespace AuthSocial.Controllers { public class IdentityController<TUser, TDbContext> :Controller where TUser :IdentityUser where TDbContext :IdentityDbContext, new() { public IdentityController() :this(new UserManager<TUser>( new UserStore<TUser>(new TDbContext()))) { } public IdentityController(UserManager<TUser> userManager) { UserManager = userManager; } protected UserManager<TUser> UserManager { get; set; } protected IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } } protected async Task SignInAsync( TUser user, bool isPersistent) { AuthenticationManager.SignOut( defaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, identity); } protected ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); } protected ClaimsIdentity GetBasicUserIdentity(string name) { var claims = new List<Claim> { new Claim(ClaimTypes.Name, name) }; return new ClaimsIdentity( claims, DefaultAuthenticationTypes.ApplicationCookie); } } }

使用泛型和相关约束会绑定 IdentityController 类,从而可以使用基于 IdentityUser 类的任何用户定义和基于 IdentityDbContext 类的任何存储上下文定义。

具体而言,IdentityUser 就是用于描述用户并提供一组最基本的属性的基类。您可以通过简单的继承关系新建和创建更多的特定类。当您这样做时,只需在衍生自 IdentityController 的类的声明中指定自定义类即可。下面是基于 IdentityController<TUser, TDbContext> 的 LoginController 和 AccountController 声明:

public class LoginController :IdentityController<IdentityUser, IdentityDbContext> { ...} public class AccountController :IdentityController<IdentityUser, IdentityDbContext> { ... }

这样,您就为在 3 个类之间对所要分担的大部分身份验证和用户管理代码复杂性进行划分奠定了基础。此外,您现在拥有一个提供常见服务的类和一个只侧重登录任务的类。最后,您还有一个单独的类来处理用户帐户管理。我就从帐户类入手,为简单起见,我只揭示用于注册新用户的端点。

AccountController 类

通常,您只有在最终用户与显示的 UI 的某些部分进行交互时才需要使用控制器方法。如果用户可以单击提交按钮,那么您需要使用区分 POST 的控制器方法。为了定义此方法,请从它的视图模型入手。视图模型类是普通的数据传输对象 (DTO),用于收集传入和传出显示的 UI 的任何数据(字符串、日期、数字、布尔和集合)。对于将新用户添加到本地成员身份系统的方法,您可以有以下视图模型类(源代码与您从默认 Visual Studio 2013 ASP.NET MVC 5 模板中获取的代码相同):

public class RegisterViewModel { public string UserName { get; set; } public string Password { get; set; } public string ConfirmPassword { get; set; } }

出于验证和显示目的,您可能还希望使用数据注释来修饰此类。但从更高的设计层次来看,最重要的是此类为实际 UI 中的每个输入控件都配备了一个属性。图 4 介绍了用于处理注册新用户的 POST 请求的控制器方法的实施。

图 4:注册新用户

public async Task<ActionResult> Register( RegisterViewModel model) { if (ModelState.IsValid) { var user = new IdentityUser { UserName = model.UserName }; var result = await UserManager.CreateAsync( user, model.Password); if (result.Succeeded) { await SignInAsync(user, false); return RedirectToAction("Index", "Home"); } Helpers.AddErrors(ModelState, result); } return View(model); }

UserManager 类上的 CreateAsync 方法只使用基础存储机制就能为特定用户创建新条目。此 AccountController 类也是用于定义其他方法来编辑或仅删除用户帐户以及更改或重置密码的理想之处。

LoginController 类

登录控制器具有用于以应用程序接受的任何方式将用户登录和注销的方法。类似于我刚才谈及的帐户控制器情况,登录方法会通过属性管理 DTO,从而传输凭据、身份验证持久性标识和返回 URL。在默认代码中,返回 URL 通过 ViewBag 进行管理;您也可以在视图模型类中填充它:

public class LoginViewModel { public string UserName { get; set; } public string Password { get; set; } public bool RememberMe { get; set; } public string ReturnUrl { get; set; } }

将与本地成员身份系统交互、完全在应用程序服务器上管理的代码是粘贴自默认项目。下面摘录了新登录方法的 POST 实施:

var user = await UserManager.FindAsync( model.UserName, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); }

最终使用户登录的代码是一个名为 SignInAsync 的方法,它是从默认实施中借用,并在 IdentityController 基类中重写为一个受保护的方法:

protected async Task SignInAsync(TUser user, bool isPersistent) { AuthenticationManager.SignOut( DefaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, identity); }

使用 ASP.NET 标识框架时,您不使用与 ASP.NET 机制明确相关的任何类。在 ASP.NET MVC 主机的上下文中,您仍通过登录窗体和 ASP.NET 身份验证 Cookie 管理用户身份验证,不过您不直接使用 FormsAuthentication ASP.NET 类。传统型 ASP.NET 开发人员需要处理的代码分为两层。一层是由 UserManager 和 IdentityUser 等类表示的普通 ASP.NET 标识 API。另一层是 OWIN Cookie 中间件,它就为外层身份验证 API 免除了登录用户的信息存储和保留方式的详细信息。窗体身份验证仍会使用,而且之前熟悉的 .ASPXAUTH Cookie 也仍会创建。不过,现在一切都在一组新方法的“幕后”发生。若要深入了解此情况,请参阅 Brock Allen 的博客文章“面向 ASP.NET 开发人员的 OWIN Cookie 身份验证中间件入门”(bit.ly/1fKG0G9)。

请查看以下直接从 Startup­Auth.cs 调用的代码(OWIN 中间件的入口点):

app.UseCookieAuthentication( new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Login/SignIn") }); app.UseExternalSignInCookie( DefaultAuthenticationTypes.ExternalCookie); app.UseTwitterAuthentication( consumerKey:"45c6...iQ", consumerSecret:"pRcoXT...kdnU");

此代码属于您使用的 Fluent 界面,而不是旧式 web.config 身份验证入口。此外,它还与基于 Twitter 的身份验证相关联,并将 OWIN 中间件配置为使用 ExternalSignInCookie 临时存储用户通过第三方登录提供程序进行登录的信息。

对象 AuthenticationManager 表示 OWIN 中间件和外层的入口点,通过此入口点,您可以调用基于 FormsAuthentication 的基础 ASP.NET 身份验证 API。

净效应是您最终会使用统一的 API 对用户进行身份验证并验证凭据,无论成员身份系统是 Web 服务器的本地系统,还是委托给外部的社交网络登录提供程序。如果通过您网站的用户的 Twitter 帐户对其进行身份验证是可接受的,您可以使用以下代码:

[AllowAnonymous] public ActionResult Twitter(String returnUrl) { var provider = "Twitter"; return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Login", new { ReturnUrl = returnUrl })); }

首要任务是,您需要配置 Twitter 应用程序,并将其与 Twitter 开发人员帐户相关联。有关详细信息,请访问 dev.twitter.com。Twitter 应用程序通过一对字母数字字符串(称为使用者密钥和密码)进行唯一标识。

您从 ASP.NET MVC 5 项目向导中获取的默认代码还建议您使用一个全新的类(即 ChallengeResult)来处理外部登录,如图 5 所示。

图 5:ChallengeResult 类

public class ChallengeResult :HttpUnauthorizedResult { public ChallengeResult(string provider, string redirectUri) { LoginProvider = provider; RedirectUri = redirectUri; } public string LoginProvider { get; set; } public string RedirectUri { get; set; } public override void ExecuteResult(ControllerContext context) { var properties = new AuthenticationProperties { RedirectUri = RedirectUri }; context.HttpContext .GetOwinContext() .Authentication .Challenge(properties, LoginProvider); } }

此类在名为 Challenge 的方法中将以下任务委托给 OWIN 中间件:连接到启动时注册的特定登录提供程序以执行身份验证。返回 URL 是登录控制器上的 ExternalLoginCallback 方法:

[AllowAnonymous] public async Task<ActionResult> ExternalLoginCallback( string returnUrl) { var info = await AuthenticationManager.GetExternalLoginInfoAsync(); if (info == null) return RedirectToAction("SignIn"); var identity = GetBasicUserIdentity(info.DefaultUserName); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = true }, identity); return RedirectToLocal(returnUrl); }

GetExternalLoginInfoAsync 方法检索登录提供程序提供的已标识用户的信息。在方法返回信息后,应用程序就有足够的信息在实际情况下将用户登录,但是什么也没有发生。请务必注意您在此处可以利用的选项。首先,您只需继续操作并创建规范的 ASP.NET 身份验证 Cookie,如我的示例所示。在 IdentityController 基类中定义的帮助程序方法 GetBasicUserIdentity 创建关于所提供的 Twitter 帐户名的 ClaimsIdentity 对象:

protected ClaimsIdentity GetBasicUserIdentity(string name) { var claims = new List<Claim> { new Claim( ClaimTypes.Name, name) }; return new ClaimsIdentity( claims, DefaultAuthenticationTypes.ApplicationCookie); }

在 Twitter 用户授权 Twitter 应用程序共享信息后,网页便会收到用户名以及可能的其他数据,如图 6 所示(具体取决于实际使用的社交 API)。

The Classic Twitter-Based Authentication
图 6:经典的基于 Twitter 的身份验证

请注意,在以上代码的 ExternalLoginCallback 方法中,没有用户添加到本地成员身份系统中。这是最简单的情况。在其他情况下,您不妨使用 Twitter 信息以编程方式注册本地登录和外部经过身份验证的用户名之间的关联(此为向导中的默认身份验证代码提供的解决方案)。最后,您可以决定将用户重定向到其他页面,以便用户能够输入电子邮件地址或仅注册您的网站。

混合经典和社交身份验证

几乎每个网站都有必要在一定程度上进行身份验证。通过 ASP.NET 标识,可以在统一外层后轻松进行编码以及混合经典和社交身份验证。不过,请务必就用户帐户处理方式以及要收集和存储的用户信息制定明确的策略。混合经典和社交身份验证意味着您可以拥有单个用户帐户,并将此帐户与多个登录提供程序(如 Facebook、Twitter)及 ASP.NET 标识支持的其他所有登录提供程序相关联。这样,用户就可以通过多种方式进行登录,但仍被您的应用程序视为唯一。此外,您还可以在方便时添加或删除更多登录提供程序。

Dino Esposito是《Architecting Mobile Solutions for the Enterprise》(Microsoft Press,2012 年)和 Microsoft Press 即将出版的《Programming ASP.NET MVC 5》的作者。作为 JetBrains 的 .NET Framework 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents.wordpress.com 上以及 twitter.com/despos 上的推文中分享他对于软件的愿景。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Pranav Rastogi