從ASP.NET 4開始,ASP.NET提供了一個至關有用的身份系統。若是您建立一個新項目並選擇一個MVC項目並選擇添加內部和外部身份驗證,那麼在您的應用程序中得到合理的身份實現是至關直接的。git

可是,若是您有現有的應用程序,或者基於完整實體框架的身份結構不適合您,那麼鏈接使用您本身的域/業務模型和類的最小和自定義實現的過程並不徹底如此直截了當。您必須從完整的模板安裝中刪除不須要的部分,或者添加必要的部分。在這篇文章中,我但願我能告訴你如何作後者,只顯示你須要的部分。github

這個過程並不必定很難 - 但它沒有很好的記錄。很難找到建立必要處理程序所需的信息,這些處理程序能夠處理將賬戶連接到外部oAuth提供程序,如Google,Facebook,GitHub等。因此在這篇文章中,我將討論這個場景。web

真實世界的用例

我在上週末本身走下了這條路,由於個人舊網站之一 - CodePaste.net - 須要更新我在該網站上運行的舊OpenID身份驗證。Codepaste是一個很是古老的網站; 事實上它是我建造的第一個MVC應用程序😃。但它在過去的7年裏一直沒有受到阻礙。直到谷歌決定拔掉舊的OpenId實施。我很快就收到了大量的電子郵件,並決定我必須從新實施外部提供商。ajax

舊的實現使用了DotNetOpenAuth幾年前我寫博客的 FormsAuthentication ,但多年來DotnetOpenAuth彷佛已經失寵了,因此我決定咬緊牙關並轉而使用更新的,內置的基於OWIN的Identity功能。 ASP.NET 4。sql

當你第一次開始查看身份時,那裏的信息量至關龐大。不少介紹文章談論如何使用'原樣'沒有定製。可是,若是沒有完整的UserManager和Entity Framework數據存儲,就沒有不少關於使用核心Identity片斷的信息,以便僅使用身份驗證/受權並將它們與我本身的業務對象/域模型集成。數據庫

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

這篇文章的目的是展現OWIN Identity系統的最小部分,以處理本地和外部賬戶登陸並將它們掛鉤到自定義域模型,而不是使用基於Entity Framework的UserManager。簡而言之,重點關注管理身份驗證的系統組件,並將用戶管理留給應用程序。編程

只要告訴我我須要什麼!

本文的主要內容很長,由於除了核心登陸功能以外,它還涵蓋了與討論相關的CodePaste.NET登陸和用戶管理功能。對於那些只須要螺母和螺栓的人來講,最後還有一個摘要部分,它只顯示骨架格式的相關代碼,您能夠將其插入控制器。我還連接到我在這裏討論的賬戶控制器的完整源代碼,若是你想看到完整的代碼,而不是一口大小的片斷。json

有關詳細信息,請繼續閱讀。後端

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的應用程序繼續。

最後我不得不說,雖然我花了很長時間才能徹底理解我須要實施的東西,但並非很困難。困難的部分只是經過挖掘生成代碼並刪除相關部分來找到您須要實現的內容的正確信息。一旦您知道須要什麼,實際代碼片斷的實現就相對簡單了。

我但願這篇文章可以很好地總結所需內容,尤爲是最小代碼摘要,建立將本身的域驅動用戶管理插入核心身份框架所需的框架代碼會更容易。

資源