从ASP.NET 4开始,ASP.NET提供了一个相当有用的身份系统。如果您创建一个新项目并选择一个MVC项目并选择添加内部和外部身份验证,那么在您的应用程序中获得合理的身份实现是相当直接的。

但是,如果您有现有的应用程序,或者基于完整实体框架的身份结构不适合您,那么连接使用您自己的域/业务模型和类的最小和自定义实现的过程并不完全如此直截了当。您必须从完整的模板安装中删除不需要的部分,或者添加必要的部分。在这篇文章中,我希望我能告诉你如何做后者,只显示你需要的部分。

这个过程并不一定很难 - 但它没有很好的记录。很难找到创建必要处理程序所需的信息,这些处理程序可以处理将帐户链接到外部oAuth提供程序,如Google,Facebook,GitHub等。所以在这篇文章中,我将讨论这个场景。

真实世界的用例

我在上周末自己走下了这条路,因为我的旧网站之一 - CodePaste.net - 需要更新我在该网站上运行的旧OpenID身份验证。Codepaste是一个非常古老的网站; 事实上它是我建造的第一个MVC应用程序😃。但它在过去的7年里一直没有受到阻碍。直到谷歌决定拔掉旧的OpenId实施。我很快就收到了大量的电子邮件,并决定我必须重新实施外部提供商。

旧的实现使用了DotNetOpenAuth几年前我写博客的 FormsAuthentication ,但多年来DotnetOpenAuth似乎已经失宠了,所以我决定咬紧牙关并转而使用更新的,内置的基于OWIN的Identity功能。 ASP.NET 4。

当你第一次开始查看身份时,那里的信息量相当庞大。很多介绍文章谈论如何使用'原样'没有定制。但是,如果没有完整的UserManager和Entity Framework数据存储,就没有很多关于使用核心Identity片段的信息,以便仅使用身份验证/授权并将它们与我自己的业务对象/域模型集成。

只是本地和外部登录的核心身份功能

这篇文章的目的是展示OWIN Identity系统的最小部分,以处理本地和外部帐户登录并将它们挂钩到自定义域模型,而不是使用基于Entity Framework的UserManager。简而言之,重点关注管理身份验证的系统组件,并将用户管理留给应用程序。

只要告诉我我需要什么!

本文的主要内容很长,因为除了核心登录功能之外,它还涵盖了与讨论相关的CodePaste.NET登录和用户管理功能。对于那些只需要螺母和螺栓的人来说,最后还有一个摘要部分,它只显示骨架格式的相关代码,您可以将其插入控制器。我还链接到我在这里讨论的帐户控制器的完整源代码,如果你想看到完整的代码,而不是一口大小的片段。

有关详细信息,请继续阅读。

ASP.NET MVC 5中基于OWIN的标识

如果您还没有在ASP.NET MVC 5中使用过新的Identity功能,我建议您首先通过创建一个新的MVC Web站点并让它创建一个启用了单个用户身份验证的默认Web站点来检查它。这将让您了解默认实现的工作原理。如果您正在查看特定于代码的实现细节,可以查看处理用户管理界面的AccountController和ManageController。AccountController处理登录本地和外部帐户,而ManageController处理设置新帐户,电子邮件确认等。这是身份感觉非常压倒性的部分原因 - 当您查看代码时,会发生大量事情并且它与基于特定实体框架的实现紧密混合。

不过,我鼓励你至少简单地看一下它,也许会逐步了解登录处理的总体流程。

只提取必要的碎片

为了在您自己的应用程序中使用不使用基于EF的UserManager的OWIN Idenity片段,您必须做一些事情。同样,作为本文的一部分,我的目标是:

  • 支持基于Cookie的用户登录(用户名/密码)
  • 支持外部登录(Google,Github,Twitter)
  • 支持使用本地或外部帐户创建帐户的功能
  • 支持使用本地或外部帐户登录

为了使用现有应用程序实现此目的,我必须:

  • 关闭应用程序的IIS身份验证
  • 添加适当的NuGet包
  • 实施本地和外部帐户的帐户注册
  • 实施登录本地和外部帐户
  • 允许使用基于OWIN的登录机制登录

我们来看看每个步骤的样子。

关闭应用程序的IIS身份验证

ASP.NET Identity使用OWIN平台,该平台是一个不依赖于标准IIS安全性的自定义子系统。因为OWIN可以自托管,所以在这个系统中不依赖于IIS。在IIS上,OWIN使用一些动态注入模块插入IIS管道,但它基本上完全接管了应用程序的身份验证/授权过程。因此,为了使用ASP.NET Identity,您首先要做的是在web.config中关闭应用程序的标准IIS身份验证。

<system.web>   
    <authentication mode="None" />   
<system.web>

我最初没有这样做,我花了一些时间来弄清楚为什么我的应用程序不断浏览浏览器身份验证对话框而不是导航到我的登录页面或将我发送到外部提供商登录。确保标准身份验证已关闭!

添加NuGet包

如果您的现有项目没有安装任何身份功能,则需要将正确的程序集放入项目中。如果你看一个新创建的MVC项目,并在那里组装的一连串不是很明显什么实际需要得到公正的基本特征。

幸运的是,由于NuGet的强大功能,为了获得核心身份功能,您只需添加以下软件包即可获得核心身份功能所需的所有引用以及所需的一些外部提供程序。

  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Cookies

前两个包足以点亮OWIN身份框架。如果您没有在IIS上运行,请使用Microsoft.Owin.Host.SelfHost而不是SystemWeb主机。

您还需要安装特定的外部提供程序包:

  • Microsoft.Owin.Security.Google
  • Microsoft.Owin.Security.Twitter
  • Owin.Security.Providers

提供程序包添加了对您可以登录的特定外部提供程序的支持。Owin.Security.Providers 包是一个第三方库,其中包括一吨,你可以用集成附加供应商,这就是我用来支持GitHub上登录,因为这是一个开发人员为主的网站。

启动类:提供程序配置

下一步是配置OWIN管道以实际处理各种登录解决方案。为此,您必须创建一个配置各种身份验证机制的Startup类。就我而言,我支持本地用户身份验证(Cookie Auth)以及Google,Twitter和GitHub的外部提供商。

这是配置代码:

namespace CodePasteMvc
{   
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }

        // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
        public void ConfigureAuth(IAppBuilder app)
        {
            // Enable the application to use a cookie to store information for the signed in user
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/LogOn")
            });


            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
            
            // App.Secrets is application specific and holds values in CodePasteKeys.json
            // Values are NOT included in repro – auto-created on first load
            if (!string.IsNullOrEmpty(App.Secrets.GoogleClientId))
            {
                app.UseGoogleAuthentication(                
                    clientId: App.Secrets.GoogleClientId,
                    clientSecret: App.Secrets.GoogleClientSecret);
            }

            if (!string.IsNullOrEmpty(App.Secrets.TwitterConsumerKey))
            {
                app.UseTwitterAuthentication(
                    consumerKey: App.Secrets.TwitterConsumerKey,
                    consumerSecret: App.Secrets.TwitterConsumerSecret);
            }

            if (!string.IsNullOrEmpty(App.Secrets.GitHubClientId))
            {
                app.UseGitHubAuthentication(
                    clientId: App.Secrets.GitHubClientId,
                    clientSecret: App.Secrets.GitHubClientSecret);
            }

            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
        }
    }
}

这是OWIN启动类的实现。当OWIN运行时启动时,它通过Reflection查找具有Configuration(IAppBuilder app)方法的名为Startup的类,并在发现它执行该方法时。

然后,ConfigureAuth()为本地登录配置Cookie身份验证,为Google,Twitter和Github配置外部提供程序。我使用自定义ApplicationConfiguration类使用我的ApplicationConfiguration类将我的秘密值保存在JSON文件中(主要是为了使它们远离GitHub仓库),但您可以对这些值进行硬编码或从配置设置中读取它们。

每个提供程序都需要相应的应用程序ID和必须配置的密钥。为了使用外部提供程序,您必须在提供程序开发人员Web站点为每个提供程序创建一个应用程序,然后选择这些应用程序生成的密钥。以下是每个网站的链接(假设您已登录到每个网站):

请注意,您可以选择提供商。您只能在此站点上使用Cookie身份验证,或仅使用外部或两者组合。

自定义AccountController

我将讨论的与这些登录相关的所有代码都位于MVC控制器中 - 特别是AccountController。实际上需要实现3个主要功能集 - 本地登录,外部登录和实际登录/注销操作。以下是AccountController类中的大致内容:

AccountControllerOverview

还有一些更传统的Controller操作可以处理我的实现的一些支持功能 - 密码恢复和帐户激活。这是我的整个实现,因此您可以看到它比完全(矫枉过正?)身份实现相当简单 - 而且功能较少 - 您可以在默认的完整功能MVC站点中获得,这对于这篇文章来说是完美的。

登录和退出

我将首先登录和退出,因为它是所有其他操作都将使用的核心功能。只要本地或外部登录成功,用户就会登录,并且此过程实际上会创建标识用户的身份验证Cookie,并允许Identity框架确定用户是否已登录并为每个请求设置User Principal对象。

如果您在ASP.NET中使用了FormsAuthentication,则您知道有一个全局对象可以处理用户跟踪cookie的管理,该cookie将用户与帐户相关联。OWIN在IAuthenticationManager接口中有自己的身份验证管理器版本,该接口附加到HttpContext对象。要获得对它的引用,您可以使用:

HttpContext.GetOwinContext().Authentication;

此对象处理用于通过站点跟踪用户的安全cookie的创建和删除。身份cookie用于跟踪所有登录用户,无论他们是使用用户名和密码在本地登录还是使用Google等外部提供商。一旦用户通过身份验证,就会调用SignIn方法来创建cookie。在随后的请求中,基于OWIN的Identity子系统然后选择Cookie并在用户访问您的站点时向用户授权基于用户的适当IPrinciple(ClaimsPrinciple with ClaimsIdentity)。

 

身份登录使用ClaimsIdentity对象,其中包含存储在声明中的用户信息。声明提供了用户ID和名称以及要与经过身份验证的用户一起存储为缓存状态的任何其他信息。

为了简化登录,我使用了几个如下所示的辅助函数:

public void IdentitySignin(AppUserState appUserState, string providerKey = null, bool isPersistent = false)
{
    var claims = new List<Claim>();

    // create required claims
    claims.Add(new Claim(ClaimTypes.NameIdentifier, appUserState.UserId));
    claims.Add(new Claim(ClaimTypes.Name, appUserState.Name));

    // custom – my serialized AppUserState object
    claims.Add(new Claim("userState", appUserState.ToString()));

    var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);

    AuthenticationManager.SignIn(new AuthenticationProperties()
    {
        AllowRefresh = true,
        IsPersistent = isPersistent,
        ExpiresUtc = DateTime.UtcNow.AddDays(7)
    }, identity);
}

public void IdentitySignout()
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie,
                                    DefaultAuthenticationTypes.ExternalCookie);
}

private IAuthenticationManager AuthenticationManager
{
    get { return HttpContext.GetOwinContext().Authentication; }
}

登入/ SignOut

关键方法是AuthenticationManager上的SignIn()SignOut(),它们在执行请求上创建或删除应用程序cookie。SignIn()接受一个I​​dentity对象,其中包含您为其分配的任何声明。一旦用户登录,您就会收到此身份,稍后您会查看Context.User.Identity以检查授权。

关于AppUserState的一个词

Note I’m using an application specific AppUserState class to represent the logged in user’s state which is then added to the Identity’s claims! This is a custom object in my application that basically holds basic User that are the bare minimum needed by the application to display user info like name, admin status, theme etc. This object is persisted and cached inside of the ClaimsIdentity claims and therefore in the cookie, so that the data is available without having to look up a user in the database for each request.

Above I show how the AppUserState is persisted in the identity cookie. For retrieval and storage in a property on the controller,  I have a base controller that has an internal AppUserState property that is loaded up from a valid ClaimsPrincipal when a request comes in in BaseController.Initialize():

protected override void Initialize(RequestContext requestContext)
{
    base.Initialize(requestContext);

    // Grab the user's login information from Identity
    AppUserState appUserState = new AppUserState();
    if (User is ClaimsPrincipal)
    {
        var user = User as ClaimsPrincipal;
        var claims = user.Claims.ToList();

        var userStateString = GetClaim(claims, "userState");
        //var name = GetClaim(claims, ClaimTypes.Name);
        //var id = GetClaim(claims, ClaimTypes.NameIdentifier);

        if (!string.IsNullOrEmpty(userStateString))
            appUserState.FromString(userStateString);
    }
    AppUserState = appUserState;
            
    ViewData["UserState"] = AppUserState;
    ViewData["ErrorDisplay"] = ErrorDisplay;
}

The net effect is that anywhere within Controller and most Views this AppUserState object is available for user info display or visual display options.

This approach made great sense when I was using FormsAuthentication because you could effectively only store a single string value which was the serialized AppUserState value. But now ClaimsIdentity can contain multiple values as claims explicitly as a dictionary so it may be much cleaner to simply store any values as claims on the ClaimsIdentity. In the future the AppUserState code could probably be abstracted away, using a custom ClaimsIdentity instead that knows how to persist and retrieve its state from the attached claims instead. I’ll leave that exercise for another day though because AppUserState is used widely in this application.

Either way I want to make it clear that AppUserState is a custom implementation and an application specific implementation detail for my application.

使用Cookie进行本地登录

每种登录类型有两个步骤:最初创建用户和帐户,然后实际登录用户。因此,让我们从本地登录的注册过程开始。

以下是用户注册表单的内容:

报名表格

表单顶部包含本地登录,而底部包含外部提供程序登录。在此表单中,您可以选择使用外部登录进行注册,该登录使用可能从外部提供商处接收的任何数据填充本地用户注册表单。Google提供电子邮件地址,GitHub包括电子邮件和姓名,而Twitter仅提供名称。在任何一种情况下,都会在应用程序中创建新用户。

响应“ 注册”按钮的代码实际上创建了一个新帐户(如果没有验证错误),并对用户进行签名。

以下是本地用户登录代码的外观(请记住此特定于应用程序):

AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult Register(FormCollection formVars)
{
    string id = formVars["Id"];
    string confirmPassword = formVars["confirmPassword"];

    bool isNew = false;
    User user = null;
    if (string.IsNullOrEmpty(id) || busUser.Load(id) == null)
    {
        user = busUser.NewEntity();
        user.InActive = true;
        isNew = true;
    }
    else
        user = busUser.Entity;
            
    UpdateModel<User>(busUser.Entity,
        new string[] { "Name", "Email", "Password", "Theme" });
            
    if (ModelState.Count > 0)
        ErrorDisplay.AddMessages(ModelState);


    if (ErrorDisplay.DisplayErrors.Count > 0)
        return View("Register", ViewModel);

    if (!busUser.Validate())
    {
        ErrorDisplay.Message = "Please correct the following:";
        ErrorDisplay.AddMessages(busUser.ValidationErrors);
        return View("Register", ViewModel);
    }

    if (!busUser.Save())
    {
        ErrorDisplay.ShowError("Unable to save User: " + busUser.ErrorMessage);
        return View("Register", ViewModel);
    }

    AppUserState appUserState = new AppUserState();
    appUserState.FromUser(user);
    IdentitySignin(appUserState, appUserState.UserId);            

    if (isNew)
    {
        SetAccountForEmailValidation();
        ErrorDisplay.HtmlEncodeMessage = false;
        ErrorDisplay.ShowMessage(@"Thank you for creating an account...");
        return View("Register", ViewModel);
    }


    return RedirectToAction("New","Snippet");
}

这段代码与我们以前的工作方式并没有太大的不同。您检查保存是否是我们在系统中已有的用户,如果是,请更新她,或者创建一个新用户并更新新用户。

这里的关键项不同之处仅在于我如前所述设置AppUserState,然后创建调用IdentitySignIn()来验证该用户。在随后的点击中,我然后获得一个Context.User,其中包含该用户的ClaimsIdentity。

登录本地帐户

注册帐户后,您实际上可以登录。以下是登录表单的外观:

登录表格

处理登录的代码如下所示:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string email, string password, bool rememberMe, string returnUrl, bool emailPassword)
{
    if (emailPassword)
        return EmailPassword(email);

    var user = busUser.ValidateUserAndLoad(email, password);
    if (user == null)
    {
        ErrorDisplay.ShowError(busUser.ErrorMessage);
        return View(ViewModel);
    }

    AppUserState appUserState = new AppUserState()
    {
        Email = user.Email,
        Name = user.Name,
        UserId = user.Id,
        Theme = user.Theme,
        IsAdmin = user.IsAdmin
    };
    IdentitySignin(appUserState, user.OpenId, rememberMe);

    if (!string.IsNullOrEmpty(returnUrl))
        return Redirect(returnUrl);

    return RedirectToAction("New", "Snippet", null);
}

您可能会再次猜测,这里的代码只是查找用户名和密码,如果有效,则更新AppUserState对象,然后调用IdentitySignin()来记录用户。

这些工作流程与我以前使用FormsAuthentication所做的完全不同。唯一真正的区别是我正在调用  IdentitySignin()而不是FormsAuthentication.Authenticate()

现在开始你以前不容易做的有趣的东西 - 外部登录。

链接到外部登录

正如您在上面的屏幕截图中看到的那样,注册和登录表单都支持使用外部提供程序来处理帐户的身份验证。对于注册表单,可以在注册开始时执行外部登录,以从外部提供者预填充注册信息,或者将外部登录附加到现有本地帐户。

对于外部登录,当您单击任何提供程序按钮时,您将被重定向到提供程序站点(Google,Twitter,GitHub),该站点将检查您的帐户是否已登录。如果不是,您将被移至提供程序登录他们的服务器上的页面,您可以登录和/或指定您希望为请求登录的应用程序(即我的站点)提供哪种权限。单击“接受”后,服务器将在您的服务器上触发回调请求,并提供提供程序可用的声明。通常,这是提供者密钥(用户登录的标识符)以及名称或电子邮件或两者。

 

外部登录通过OAuth2流程处理,该流程由ASP.NET中的OWIN身份验证管道内部管理。当请求被触发时,它们包括一个首先触发到OWIN处理程序的回调URL。回调网址为/ signin-google或/ signin-twitter或/ signin-github。

在本地帐户和外部帐户之间创建初始帐户链接以及日志记录都有两个端点请求流:一个是通过质询操作(实际上是重定向)实际启动远程身份验证过程,另一个是接收身份验证完成后的回调。

这是第一种通过挑战和重定向到提供者来启动外部帐户链接的方法:

[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ExternalLinkLogin(string provider)
{
    // Request a redirect to the external login provider to link a login for the current user
    return new ChallengeResult(provider, Url.Action("ExternalLinkLoginCallback"), AppUserState.UserId);
}

ChallengeResult是一个帮助器类,它是ASP.NET MVC默认控制器实现的一部分,我从中简单地复制它:

private const string XsrfKey = "CodePaste_$31!.2*#";

public class ChallengeResult : HttpUnauthorizedResult
{
    public ChallengeResult(string provider, string redirectUri)
        : this(provider, redirectUri, null)
    { }

    public ChallengeResult(string provider, string redirectUri, string userId)
    {
        LoginProvider = provider;
        RedirectUri = redirectUri;
        UserId = userId;
    }

    public string LoginProvider { get; set; }
    public string RedirectUri { get; set; }
    public string UserId { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
        var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
        if (UserId != null)
            properties.Dictionary[XsrfKey] = UserId;
                
        var owin = context.HttpContext.GetOwinContext();
        owin.Authentication.Challenge(properties, LoginProvider);
    }
}

此类的关键是OWIN Authentication.Challenge()方法,该方法向提供程序发出302重定向,以使用包含重定向URL和某些状态信息的URL处理登录。在这种情况下,状态是一个用户标识符(在这种情况下是我们的用户ID),它允许我们检查并确保结果是我们感兴趣的结果。

当提供程序验证(或无法验证)用户时,它会使用特定URL回调您的服务器。回调的URL是〜/ signin-google或〜/ signin-github或〜/ signin-twitter。OWIN管道在内部为您处理此回调,并在验证重定向到您的实际端点之后,以便您可以处理经过身份验证的请求。

为了说明在登录我的Google帐户时查看此注册链接请求的Fiddler跟踪:

FiddlerRegistration

注意所有302个请求。第一个请求由您的代码使用ChallengeResult启动,后者会重定向到Google。然后,Google会重定向回OWIN内部端点以处理提供程序OAuth解析,最后使用ExternalLinkLoginCallback()将OWIN管道调用到您的代码中。

这是链接过程完成时调用的Callback方法:

[AllowAnonymous]
[HttpGet]
public async Task<ActionResult> ExternalLinkLoginCallback()
{
    // Handle external Login Callback
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, AppUserState.UserId);
    if (loginInfo == null)
    {
        IdentitySignout(); // to be safe we log out
        return RedirectToAction("Register", new {message = "Unable to authenticate with external login."});
    }

    // Authenticated!
    string providerKey = loginInfo.Login.ProviderKey;
    string providerName = loginInfo.Login.LoginProvider;

    // Now load, create or update our custom user

    // normalize email and username if available
    if (string.IsNullOrEmpty(AppUserState.Email))
        AppUserState.Email = loginInfo.Email;
    if (string.IsNullOrEmpty(AppUserState.Name))
        AppUserState.Name = loginInfo.DefaultUserName;

    var userBus = new busUser();
    User user = null;

    if (!string.IsNullOrEmpty(AppUserState.UserId))
        user = userBus.Load(AppUserState.UserId);

    if (user == null && !string.IsNullOrEmpty(providerKey))
        user = userBus.LoadUserByProviderKey(providerKey);

    if (user == null && !string.IsNullOrEmpty(loginInfo.Email))
        user = userBus.LoadUserByEmail(loginInfo.Email);

    if (user == null)
    {
        user = userBus.NewEntity();
        userBus.SetUserForEmailValidation(user);
    }

    if (string.IsNullOrEmpty(user.Email))
        user.Email = AppUserState.Email;

    if (string.IsNullOrEmpty(user.Name))
        user.Name = AppUserState.Name ?? "Unknown (" + providerName + ")";


    if (loginInfo.Login != null)
    {
        user.OpenIdClaim = loginInfo.Login.ProviderKey;
        user.OpenId = loginInfo.Login.LoginProvider;
    }
    else
    {
        user.OpenId = null;
        user.OpenIdClaim = null;
    }

    // finally save user inf
    bool result = userBus.Save(user);

    // update the actual identity cookie
    AppUserState.FromUser(user);
    IdentitySignin(AppUserState, loginInfo.Login.ProviderKey);

    return RedirectToAction("Register");
}

这段代码中有相当多的代码,但是......大多数代码都是特定于应用程序的。代码中最重要的部分是第一行,它负责返回包含providerKey的LoginInfo对象以及提供程序提供的任何其他声明。通常这些是名称和/或电子邮件。然后,其余代码将检查用户是否已存在并更新它,或者是否创建新用户并保存。如果全部通过在后续请求中创建cookie和授权的ClaimsIdentity来调用IdenitySignin()来有效地登录用户。

完成后,用户已登录,但我想重新显示注册表单以显示外部帐户注册:

LinkedAccountDisplay

使用外部登录登录

最后,我们仍然需要连接逻辑以使用外部提供程序登录。与链接提供程序一样,这是一个两步过程 - 触发初始身份验证请求。与链接操作一样,质询请求处理此问题:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider)
{
    string returnUrl = Url.Action("New","Snippet",null);            
    return new ChallengeResult(provider, Url.Action("ExternalLoginCallback",
                               "Account", new { ReturnUrl = returnUrl }));
}

同样发出相同的302个请求以最终将结果带回到OWIN管道,而OWIN管道又重定向到ExternalLoginCallback()方法:

[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
        return RedirectToAction("LogOn");

    // AUTHENTICATED!
    var providerKey = loginInfo.Login.ProviderKey;


    // Aplication specific code goes here.
    var userBus = new busUser();
    var user = userBus.ValidateUserWithExternalLogin(providerKey);
    if (user == null)
    {
        return RedirectToAction("LogOn", new
        {
            message = "Unable to log in with " + loginInfo.Login.LoginProvider +
                        ". " + userBus.ErrorMessage
        });
    }

    // store on AppUser
    AppUserState appUserState = new AppUserState();
    appUserState.FromUser(user);
    IdentitySignin(appUserState, providerKey, isPersistent: true);

    return Redirect(returnUrl);
}

该过程类似于前一个示例中的链接操作,除了登录代码只是检查用户是否有效,如果是,则使用现在熟悉的IdentySignin()方法将其登录。 

此时用户已登录,因此只需重定向我们想去的地方。

最后,如果您要取消帐户关联,我所要做的就是删除域模型中的链接。在我的情况下,我删除OpenId和OpenIdClaim的字段值,以便使用外部帐户的任何后续登录都将失败,因为我们将不匹配外部提供程序提供的提供程序密钥。

这是unlink操作的代码:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ExternalUnlinkLogin()
{
    var userId = AppUserState.UserId;
    var user = busUser.Load(userId);
    if (user == null)
    {
        ErrorDisplay.ShowError("Couldn't find associated User: " + busUser.ErrorMessage);
        return RedirectToAction("Register", new { id = userId });
    }
    user.OpenId = string.Empty;
    user.OpenIdClaim = string.Empty;

    if (busUser.Save())
        return RedirectToAction("Register", new { id = userId });

    return RedirectToAction("Register", new { message = "Unable to unlink OpenId. " + busUser.ErrorMessage });
}

执行此代码后,外部帐户链接消失。我再次显示注册页面,这次外部链接帐户不再显示由三个外部提供者再次替换,其中一个可以连接。

这就是所有的基础操作!

快速回顾

您可能认为这是很多代码,您必须编写一些非常简单的代码。但请记住,此代码包含我在此提供的一些特定于应用程序的逻辑,以便提供一些有点逼真的上下文。虽然实际的底层身份代码非常小(并且我以粗体突出显示了代码段中的核心要求),但此处显示的代码可能是您可能希望在其中运行的基本自我管理用户管理实现所需的最低限度真正的应用。

如果您想查看完整的控制器代码,可以在Github上查看。

总之,你基本上处理:

IdentitySignInIdentitySignOut,用于标准LogIn / LogOut函数

实现ExternalLinkLogin()ExternalLinkLoginCallback()

实现ExternalLogin()ExternalLoginCallback()

各种ExternalXXXX方法遵循一个简单的样板,使用ChallengeResult作为初始请求,并调用GetExternalLoginInfoAsync()来获取结果数据。只要你知道实际需要实现的部分,它就非常直接。

 

最小代码摘要

因为上面的代码非常冗长,所以这里只是基本部分实现的相关部分的摘要:

启动配置类

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureAuth(app);
    }

    // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
    public void ConfigureAuth(IAppBuilder app)
    {
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Enable the application to use a cookie to store information for the signed in user
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/LogOn")
        });
            
        // these values are stored in CodePasteKeys.json
        // and are NOT included in repro - autocreated on first load
        if (!string.IsNullOrEmpty(App.Secrets.GoogleClientId))
        {
            app.UseGoogleAuthentication(                
                clientId: App.Secrets.GoogleClientId,
                clientSecret: App.Secrets.GoogleClientSecret);
        }

        if (!string.IsNullOrEmpty(App.Secrets.TwitterConsumerKey))
        {
            app.UseTwitterAuthentication(
                consumerKey: App.Secrets.TwitterConsumerKey,
                consumerSecret: App.Secrets.TwitterConsumerSecret);
        }

        if (!string.IsNullOrEmpty(App.Secrets.GitHubClientId))
        {
            app.UseGitHubAuthentication(
                clientId: App.Secrets.GitHubClientId,
                clientSecret: App.Secrets.GitHubClientSecret);
        }

        AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
    }
}

IdentitySignIn / SignOut

public void IdentitySignin(string userId, string name, string providerKey = null, bool isPersistent = false)
{
    var claims = new List<Claim>();

    // create *required* claims
    claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
    claims.Add(new Claim(ClaimTypes.Name, name));

    var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);

    // add to user here!
    AuthenticationManager.SignIn(new AuthenticationProperties()
    {
        AllowRefresh = true,
        IsPersistent = isPersistent,
        ExpiresUtc = DateTime.UtcNow.AddDays(7)
    }, identity);
}

public void IdentitySignout()
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie,
                                  DefaultAuthenticationTypes.ExternalCookie);
}

private IAuthenticationManager AuthenticationManager
{
    get
    {
        return HttpContext.GetOwinContext().Authentication;
    }
}
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ExternalLinkLogin(string provider) //Google,Twitter etc.
{
    return new ChallengeResult(provider, Url.Action("ExternalLinkLoginCallback"), userId);
}

[AllowAnonymous]
[HttpGet]        
public async Task<ActionResult> ExternalLinkLoginCallback()
{
    // Handle external Login Callback
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey,userId);
    if (loginInfo == null)
    {
        IdentitySignout(); // to be safe we log out
        return RedirectToAction("Register", new {message = "Unable to authenticate with external login."});
    }

    // Authenticated!
    string providerKey = loginInfo.Login.ProviderKey;
    string providerName = loginInfo.Login.LoginProvider;


    // Your code here…


    
    // when all good make sure to sign in user
    IdentitySignin(userId, name, providerKey, isPersistent: true);


    return RedirectToAction("Register");
}     

此代码和外部登录还需要ChallengeResult帮助程序类:

// Used for XSRF protection when adding external logins
private const string XsrfKey = "CodePaste_$31!.2*#";

public class ChallengeResult : HttpUnauthorizedResult
{
    public ChallengeResult(string provider, string redirectUri)
        : this(provider, redirectUri, null)
    {  }

    public ChallengeResult(string provider, string redirectUri, string userId)
    {
        LoginProvider = provider;
        RedirectUri = redirectUri;
        UserId = userId;
    }

    public string LoginProvider { get; set; }
    public string RedirectUri { get; set; }
    public string UserId { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
        var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
        if (UserId != null)
            properties.Dictionary[XsrfKey] = UserId;
                
        var owin = context.HttpContext.GetOwinContext();
        owin.Authentication.Challenge(properties, LoginProvider);
    }
}

外部登录

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider)
{
    string returnUrl = Url.Action("New","Snippet",null);            
    return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", 
                               "Account", new { ReturnUrl = returnUrl }));
}
        
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{                        
    if ( string.IsNullOrEmpty(returnUrl) )
        returnUrl = "~/";
            
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
        return RedirectToAction("LogOn");

    // AUTHENTICATED!
    var providerKey = loginInfo.Login.ProviderKey;

    // Your code goes here.

    // when all good make sure to sign in user
    IdentitySignin(userId, name, providerKey, isPersistent: true);

    return Redirect(returnUrl);
}

本地帐户注册

[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult Register(FormCollection formVars)
{

    // Capture User Data and Create/Update account
     
    // when all good make sure to sign in user
    IdentitySignin(userId, name, appUserState.UserId);            

    return RedirectToAction("New","Snippet",null);
}cs

本地帐户登录

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string email, string password, bool rememberMe, string returnUrl, bool emailPassword)
{
    // validate your user 
    
    // if all OK sign in
    IdentitySignin(userId, name, user.OpenId, rememberMe);

    return RedirectToAction("New", "Snippet", null);
}

public ActionResult LogOff()
{
    IdentitySignout();
    return RedirectToAction("LogOn");
}

摘要

对于新项目而言,无论是想要自​​己动手,还是坚持使用股票身份实施,都可能需要长时间的思考。就个人而言,我不是开箱即用的基于EF的实现的粉丝。虽然可以自定义,但仍需要大量调整UI以使其适合您的应用程序,并可能添加和删除Identity模型中的字段。最糟糕的是,默认的EF依赖关系不容易集成到另一个EF模型中。就个人而言,我更喜欢将用户管理集成为我自己的应用程序域模型的一部分,而不是将用户链接到客户。

使用自己的另一个好处是,你不会坚持微软的想法。我们经历了太多的身份验证框架,微软似乎在每个主要的发布周期都在改变模型。虽然新身份可能是他们曾经拥有的最接近实际可用的东西,但是我仍然需要依赖微软在这方面所做的任何事情,因为他们害怕将它从下面拉出来我在下一个版本。使用我自己的,我不必担心至少可以随我的应用程序一起旅行的用户管理功能。使用我自己的实现,我可能会有更多的设置,但至少我有一个标准的方法,我可以轻松地通过任何版本的ASP.NET的应用程序继续。

最后我不得不说,虽然我花了很长时间才能完全理解我需要实施的东西,但并不是很困难。困难的部分只是通过挖掘生成代码并删除相关部分来找到您需要实现的内容的正确信息。一旦您知道需要什么,实际代码片段的实现就相对简单了。

我希望这篇文章能够很好地总结所需内容,尤其是最小代码摘要,创建将自己的域驱动用户管理插入核心身份框架所需的框架代码会更容易。

资源