原文出自 "Rui Figueiredo" 的博文 "《External Login Providers in ASP.NET Core》" (本文很長) 摘要:本文主要介紹了使用外部登陸提供程式登陸的流程,以及身份認證的流程。 為了能夠使用google、facebook、twitter、微博等外部 ...
原文出自Rui Figueiredo的博文《External Login Providers in ASP.NET Core》 (本文很長)
摘要:本文主要介紹了使用外部登陸提供程式登陸的流程,以及身份認證的流程。
為了能夠使用google、facebook、twitter、微博等外部登陸提供程式,從而避免創建本地賬戶以及電子郵件驗證等繁瑣步驟,我們一般會引用到外部登陸服務,將驗證用戶身份的任務委托給他們。外部驗證最為流行的協議就是OAuth2和OpenId Connect。
在Asp.Net中使用外部登陸提供商的文檔非常少,更糟糕的是當地使用“File -> New Project”創建項目所生成的模板代碼也很複雜,並不容易看得懂然後照著做。而且如果你不瞭解身份認證中間件在Asp.Net中是如何工作的,那麼基本上是不可能弄懂那些模板代碼的。
為了真正瞭解如何在Asp.Net中使用外部登陸,那麼必須先理解中間件管道以及特定的身份認證中間件是如何工作的,以及一點OAuth協議。
本博客文章解釋了所有這些部分是如何組合在一起的,並提供了有關如何利用身份驗證中間件和外部登錄提供程式本身和結合ASP.NET Core Identity的示例。
中間件管道
當一個請求進入Asp.Net Core程式,請求會通過由中間件組成的中間件管道。管道中的每個中間件都“有機會(譯者註:如果一個中間件短路了那麼後續的中間件就沒機會了)”檢查、處理請求,傳遞到下一個中間件,然後在後面的中間件都執行之後再做些額外的操作。
管道在Startup
類中的Config
方法中定義,下麵是一個添加到管道中的中間件的例子:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.Use(async (HttpContext context, Func<Task> next) =>
{
// 在執行下一個中間件之前做些事
await next.Invoke(); // 下一個中間件做的事
// 在執行下一個中間件之後做些事
});
}
需要註意的一件重要的事情是所有的中間件都可以訪問HttpContext
的實例。
通過這個httpContext
實例,他們可以向其它的中間件“發送”信息。例如,如果管道末端的中間件通過執行類似HttpContext.Items[“LoginProvider”] =“Google
”的方式來更改HttpContext
,則所有位於其之前的中間件都將能夠訪問該值。
另一個重要的事情是,任何中間件都可以停止管道(短路),即它可以選擇不調用下一個中間件。這對外部登錄提供程式(external login provider)尤其重要。
例如,如果你用Google作為你的外部登錄提供程式,則用戶將在成功驗證後重定向到http://YourAppDomain.com/signin-google
。如果你已經嘗試了(使用預設的Visual Studio模板生成的代碼)使用外部登錄提供程式(本例子使用的是Google),那麼你可能已經註意到沒有Controller
或者Action
,或者看起來沒有其他任何響應上述URL的內容。
發生了什麼呢?其實 GoogleAuthentication
中間件查找該URL,並且當它發現它時 GoogleAuthentication
中間件將“接管”請求,然後也不會調用管道中的任何其他中間件,即MVC中間件。
作為這種行為的結果,中間件運行的順序非常重要。
想象一下,你的程式支持多個外部登錄提供程式(例如Facebook和Google)的情況。當他們運行時,需要有一個中間件,即 CookieAuthentication
中間件,它能夠將他們放入HttpContext
中的信息轉換成代表登錄用戶的cookie(本文後面給出了示例)。
The Authentication Middleware
使中間件成為認證中間件的原因是它繼承了一個名為AuthenticationMiddleware
的類,這個類只是創建一個AuthenticationHandler
。大部分身份認證功能都在AuthenticationHandler
裡面。
儘管我們不打算描述如何創建自己的身份驗證中間件,我們將描述身份驗證中間件如何進行交互,以及當你有多個認證中間件在管道中時,他們如何相互交互。
在添加AuthenticationMiddleware時,你最少要指定三個值
AuthenticationScheme
AutomaticAuthenticate
標誌AutomaticChallenge
標誌
你可以將 AuthenticationScheme
視為身份驗證中間件的名稱。 在以前的ASP.NET版本中,這被稱為authentication type。
AutomaticAuthenticate
標誌指定管道中的中間件應該在它拿到請求時就立即“認證”用戶。例如,如果使用 AutomaticAuthenticate = true
將cookie 中間件添加到管道,則會在請求中查找 authentication cookie,並使用它創建 ClaimsPrincipal
並將其添加到 HttpContext
。順便說一句,這就是讓用戶“登錄”的原因。
如果你要使用 AutomaticAuthenticate = false
設置 cookie 中間件,並且在該cookie中間件的請求中有一個 authentication cookie,則用戶不會自動“登錄”。
在以前的ASP.NET版本中,具有 AutomaticAuthenticate = true
的認證中間件被稱為active認證中間件,而 AutomaticAuthenticate = false
被稱為passive認證中間件。
The Challenge
你可以“Challenge”一個身份驗證中間件。這是一個在ASP.NET Core之前不存在的新術語。我不知道把它稱為Challenge的原因,所以我不會試圖描述為什麼這樣叫。相反,我會給你一些中間件被“Challenged”時會發生什麼事情的例子。
譯者註: challenge 有 挑戰的意思,也有 質疑,質詢,對...質詢的意思,記住它的其他意思,會對你理解下文有幫助
例如,Cookie中間件在“Challenged”時會將用戶重定向到登錄頁面。Google身份驗證中間件返回302響應,將用戶重定向到Google的OAuth登錄頁面。通常challenge 認證中間件,你需要給它命名(通過它的AuthenticationScheme
屬性)。例如,要challenge 一個帶有 AuthenticationScheme =“Google”
身份驗證中間件,你可以在controller action 中執行此操作:
public IActionResult DoAChallenge()
{
return Challenge("Google");
}
但是,你可以發出一個“naked”的challenge(即不命名任何認證中間件,例如返回Challenge
),然後具有AutomaticChallenge = true
的認證中間件將是被選中的認證中間件。
與認證中間件進行交互
Challenge只是可以在認證中間件上“執行(performed)”的操作之一。The others are Authenticate, SignIn and SignOut.
例如,如果你向身份驗證中間件“發起(issue)” 身份驗證(Authenticate )操作(假設此示例在controller action中):
var claimsPrincipal = await context.Authentication.AuthenticateAsync("ApplicationCookie");
譯者註:
context.Authentication.AuthenticateAsync
在2.0中已經過時,只需將其修改為context.AuthenticateAsync
即可,不過返回值類型已經由ClaimsPrincipal
變為AuthenticateResult
,不過AuthenticateResult
中含有ClaimsPrincipal
, 參考信息
這將導致中間件嘗試認證並返回一個ClaimsPrincipal
。例如,cookie中間件會在請求中查找cookie,並使用cookie中包含的信息構建 ClaimsPrincipal
和 ClaimsIdentity
。
一般來講,如果給認證中間件配置了AutomaticAuthenticate = false
,那麼你需要手動發起認證。
也可以發起(issue)SignIn:
await context.Authentication.SignInAsync("ApplicationCookie", claimsPrincipal);
譯者註:這個也過時了,參考上一個
如果“ApplicationCookie”是一個cookie中間件,它將修改響應,以便在客戶端創建一個cookie。該cookie將包含重新創建作為參數傳遞的 ClaimsPrincipal
所需的所有信息。
最後,SignOut,例如,cookie中間件將刪除標識用戶的cookie。下麵這段代碼展示瞭如何在名為“ApplicationCookie”的身份驗證中間件上調用註銷(sign out)的示例:
await context.Authentication.SignOutAsync("ApplicationCookie"/*這裡是中間件的AuthenticationScheme*/);
譯者註:這個也過時了,參考上一個
中間件交互示例
如果沒有示例,那麼很難想象這些東西是如何組合在一起的,接下來將展示一個使用cookie身份驗證中間件的簡單示例。
使用 cookie 認證中間件登陸用戶
以下是Cookie身份驗證和MVC中間件的設置:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions{
AuthenticationScheme = "MyCookie",
AutomaticAuthenticate = true,
AutomaticChallenge = true,
LoginPath = new PathString("/account/login")
});
app.UseMvcWithDefaultRoute();
}
當一個請求到達配置了這個管道的ASP.NET Core應用程式時,會發生什麼情況呢?cookie身份驗證中間件將檢查請求並查找cookie。這是因為認證中間件配置了AutomaticAuthenticate = true
。如果cookie位於請求中,則將其解密並轉換為ClaimsPrincipal
併在將其設置到HttpContext.User
上。之後,cookie中間件將調用管道中的下一個中間件,本例中是MVC。如果cookie不在請求中,cookie中間件將直接調用MVC中間件。
如果用戶執行了帶有[Authorize]屬性註釋的controller action 請求,且用戶未登錄(即未設置HttpContext.User),例如:
[Authorize]
public IActionResult ActionThatRequiresAnAuthenticatedUser()
{
//...
}
一個 challenge 會被髮起(issue),並且含有 AutomaticChallenge = true
的認證中間件會處理它。cookie中間件通過將用戶重定向到LoginPath(將狀態碼設為302,和Location 頭設為/account/login)來響應challenge。
或者,如果你的身份驗證中間件未設置為AutomaticChallenge = true
,並且你想“challenge”它,則可以指定AuthenticationScheme
:
[Authorize(ActiveAuthenticationSchemes="MyCookie")]
public IActionResult ActionThatRequiresAnAuthenticatedUser()
{
//...
}
譯者註:
ActiveAuthenticationSchemes
已經過時,使用AuthenticationSchemes
替換
為了涵蓋所有可能的方式來發出challenge,你也可以使用控制器中的Challenge
方法:
public IActionResult TriggerChallenge()
{
return Challenge("MyCookie");
}
用這種方法手動發起challenge時需要註意一件重要事。如果你對身份驗證中間件(例如“MyCookie”)發出了一個challenge,然後身份驗證中間件“將用戶登入”(在這種情況下,請求中有一個對應這個中間件的cookie),那麼中間件會將challenge作為響應未經授權的訪問,並將用戶重定向到/Account/ccessDenied
。你可以通過在CookieAuthenticationOptions
中設置AccessDeniedPath
來更改該路徑。
這背後的原因是,如果用戶已經登錄,並且向簽入該用戶的中間件發出challenge,則這意味著用戶沒有足夠的許可權(例如,不具有所需的角色)。
以前版本的ASP.NET中的行為是將用戶重定向回登錄頁面。但是,如果使用外部登錄提供程式,則會造成問題。
外部登錄提供程式會“記住”你已經登錄。這就是為什麼如果你已經登錄到Facebook,並且你使用了一個允許你登錄Facebook的網路應用,你將被重定向到Facebook,然後立即返回到網路應用(假設你已經授權在Facebook的網路應用程式)。如果你沒有足夠的許可權,可能會導致重定向迴圈。因此,在這些情況下,為了避免導致重定向迴圈,ASP.NET Core中的身份驗證中間件會將用戶重定向到拒絕訪問頁面。
使用外部登陸提供器中間件
依賴外部登錄提供程式時,最簡單的設置是配置一個cookie身份驗證中間件,負責對用戶進行登陸。然後再配置一個我們要使用的特定外部登錄提供程式的中間件。
如果我們想要使用Google登陸,我們可以像這樣配置我們的管道:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions{
AuthenticationScheme = "MainCookie",
AutomaticAuthenticate = true,
AutomaticChallenge = false
});
app.UseGoogleAuthentication(new GoogleOptions{
AuthenticationScheme = "Google",
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET",
CallbackPath = new PathString("/signin-google"),
SignInScheme = "MainCookie"
});
app.UseMvcWithDefaultRoute();
}
譯者註:UseXyzAuthentication系列擴展方法已經過時,取而代之的是在ConfigService中的AddXyz()系列
例如:public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) { app.UseIdentity(); app.UseCookieAuthentication(new CookieAuthenticationOptions { LoginPath = new PathString("/login") }); app.UseFacebookAuthentication(new FacebookOptions { AppId = Configuration["facebook:appid"], AppSecret = Configuration["facebook:appsecret"] }); }
替換為
public void ConfigureServices(IServiceCollection services) { services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores(); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(o => o.LoginPath = new PathString("/login")) .AddFacebook(o => { o.AppId = Configuration["facebook:appid"]; o.AppSecret = Configuration["facebook:appsecret"]; }); } public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) { app.UseAuthentication(); }
每當有這個配置的請求進來,它將“通過”cookie中間件,cookie 中間件將檢查它尋找一個屬於他的cookie。cookie的名字決定了cookie是否屬於特定的中間件。預設的是將AuthenticationScheme
加上.AspNetCore.。所以對於MainCookie 這個cookie的名字就是.AspNetCore.MainCookie。
如果請求中沒有cookie,cookie身份驗證中間件只是調用管道中的下一個中間件。在這個例子中是Google身份驗證中間件。我們在這個例子中將Google身份驗證中間件命名為“Google”。當我們使用外部登錄提供者時,提供者必須知道我們的Web應用程式。總會有一個步驟,外部登陸提供者讓你註冊你的應用程式,你會得到一個ID和一個Secret (我們稍後將會詳細說明為什麼需要這些東西)。在示例是ClientId和ClientSecret屬性。
接下來我們定義了一個CallbackPath。當用戶使用外部登錄提供程式成功登錄時,外部登錄提供程式會發出重定向,以便將用戶重定向回 發起登錄進程的Web應用程式。CallbackPath 必須與外部登錄提供程式將用戶重定向到的位置 相匹配(稍後你會明白)。
最後,SignInScheme指定在認證成功後,Google認證中間件將使用哪一個AuthenticationScheme
發起SignIn。
外部登錄提供商中間件將“干預”請求的唯一情況是中間件被“challenged”或請求與CallbackPath匹配。
我們先來看看這個challenge。想象一下你有一個像這樣的controller action:
public IActionResult SignInWithGoogle()
{
var authenticationProperties = new AuthenticationProperties{
RedirectUri = Url.Action("Index", "Home")
};
return Challenge(authenticationProperties, "Google");
}
當你發起challenge時,你可以指定AuthenticationProperties
的一個實例。AuthenticationProperties
類允許你指定用戶在成功驗證的情況下應該重定向到的其他選項。當發出這個challenge時,Google Authentication 中間件會將響應狀態代碼更改為302然後重定向到Google的OAuth2登錄URL。它看起來像這樣:
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http%3A%2F%www.yourdomain.com%2Fsignin-google&scope=openid%20profile%20email&state=....
然後用戶登錄/授權Web應用程式,然後Google將其重定向回Web應用程式。例如,如果你在Google註冊你的網路應用程式時將重定向URI定義為http://www.yourdomain.com/signin-goole
,那麼在用戶成功通過Google身份驗證之後,他將被重定向到。http://www.yourdomain.com/signin-goole
。
當請求到來時,如果配置正確,它將匹配 CallbackPath(/signin-google),然後Google Authentication 中間件將接管該請求。
這個請求看起來可能是這樣:
http://www.yourdomain.com/signin-google?state=…&code=4/j5FtSwx5qyQwwl8XQgi4L6LPZcxxeqgMl0Lr7bG8SKA&authuser=0&session_state=…&prompt=none
查詢字元串中的code值將用於向Google發出請求並獲取有關用戶的信息(這是OAuth2協議的一部分,將在下一部分中進行更詳細的說明)。請註意,這是由Web應用程式向Google發送的請求。這對用戶是透明的。通過對該請求(使用代碼的那個)的響應,GoogleAuthentication中間件創建一個ClaimsPrincipal並調用配置中間件時提供的SignInScheme“登錄”。最後,響應被更改為302重定向到challenge中的AuthenticationProperties中指定的重定向URL(在本例中是Home控制器中的Index aciton)。
使用額外的Cookie中間件來啟用中間認證步驟
如果你曾嘗試將預設Visual Studio模板與外部登錄提供程式一起使用,那麼你可能已經註意到,如果使用外部登錄提供程式進行身份驗證,則會將你帶到要求你創建本地用戶帳戶的頁面。
用戶在登錄之前必須經過這個中間步驟。
這是通過使用兩個cookie身份驗證中間件來實現的。
一個主動查找請求中的cookie,並登錄用戶(AutomaticAuthenticate = true)。這個通常被稱為ApplicationCookie,或者在我們的例子中叫做MainCookie。而另一個是被動的(AutomaticAuthenticate = false
,即它不會自動設置HttpContext.User
與各個Cookie中的ClaimsIdentity
用戶)。這個通常被稱為ExternalCookie
,因為它是外部登錄提供者發起“登錄”的地方。
外部登錄提供程式的SignInScheme設置為external cookie中間件(使用AutomaticAuthenticate = false
配置的中間件),並設置RedirectUri到指定的controller action,由這個action“手動”調用該SignInScheme中的“Authentication”來發起challenge。
下麵是示例:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions{
AuthenticationScheme = "MainCookie",
AutomaticAuthenticate = true,
AutomaticChallenge = false
});
app.UseCookieAuthentication(new CookieAuthenticationOptions{
AuthenticationScheme = "ExternalCookie",
AutomaticAuthenticate = false,
AutomaticChallenge = false
});
app.UseGoogleAuthentication(new GoogleOptions{
AuthenticationScheme = "Google",
SignInScheme = "ExternalCookie",
CallbackPath = new PathString("/signin-google"),
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET"
});
app.UseMvcWithDefaultRoute();
}
主要變化在於
AutomaticAuthenticate
和AutomaticChallenge
被替代,因為這輛屬性的意圖其實只能用在一個中間件上,即只能讓一個認證中間件,自動觸發Authenticate 或者Challenge,所以他們移除了由 AddAuthentication(option) 指定,你可以先看這篇博客,因為不影響流程理解。
這和以前的情況唯一的區別是,現在有一個額外的身份驗證中間件(ExternalCookie),外部登錄提供程式中的SignInScheme也被設置到了這個中間件。
當我們在這種情況下進行挑戰時,我們必須將用戶重定向到一個controller action,該action在ExternalCookie中“手動”觸發Authenticate。代碼看起來如下:
public IActionResult Google()
{
var authenticationProperties = new AuthenticationProperties
{
RedirectUri = Url.Action("HandleExternalLogin", "Account")
};
return Challenge(authenticationProperties, "Google");
}
Account controller中的 HandleExternalLogin 方法 :
public async Task<IActionResult> HandleExternalLogin()
{
var claimsPrincipal = await HttpContext.Authentication.AuthenticateAsync("ExternalCookie");
//do something the the claimsPrincipal, possibly create a new one with additional information
//create a local user, etc
await HttpContext.Authentication.SignInAsync("MainCookie", claimsPrincipal);
await HttpContext.Authentication.SignOutAsync("ExternalCookie");
return Redirect("~/");
}
譯者註:這裡的代碼到了2.0時略有變化,參見之前的內容
我們在這個控制器動作中所做的是在ExternalCookie中間件中“手動”觸發一個Authenticate動作。這將返回從請求中的 cookie 重建的ClaimsPrincipal
。由於我們已經設置了SignInScheme = ExternalCookie
,所以在驗證成功之後,該cookie由 Google Authentication 中間件設置。GoogleAuthentication中間件在內部將執行類似以下的操作:
HttpContext.Authentication.SignInAsync("ExternalCookie", claimsPrincipalWithInformationFromGoogle);
這就是為什麼ExternalCookie中間件創建cookie的原因。
接下來我們可以使用ClaimsPrincipal中包含的信息做一些額外的操作,例如檢查用戶(通過ClaimsPrincipal.Claims中包含的電子郵件)是否已經有本地帳戶,如果沒有將用戶重定向到提供創建本地帳戶選項的頁面(這是預設的Visual Studio模板所做的)。
在這個例子中,我們簡單地向MainCookie中間件發出SignIn操作,這將導致該Cookie中間件更改發送給用戶的響應,以便創建encoded 的ClaimsPrincipal
的cookie(即,響應將具有編碼ClaimsPrincipal
的名為.AspNetCore.MainCookie的cookie)。
請記住,這個中間件是一個具有AutomaticAuthenticate = true
的中間件,這意味著在每個請求中它將檢查它尋找一個cookie(名為.AspNetCore.MainCookie),如果它存在,它將被解碼成ClaimsPrincipal
並設置在HttpContext.User上,然後使用戶登錄。最後,我們只需發起一個SignOut到ExternalCookie中間件。這會導致中間件刪除相應的cookie。
我們從用戶的視角來回顧一下:
- 用戶請求了一個action ,這個action向Google認證中間件發起challenge,例如, /Account/SignInWithGoogle。challenge action定義了RedirectUrl,例如/Account/HandleExternalLogin
- 響應將用戶瀏覽器重定向到Google的OAuth登錄頁面
- 成功驗證和授權Web應用程式後,Google會將用戶重定向回Web應用程式。例如/signin-google?code=…
- Google身份驗證中間件將接管請求(CallBackPath匹配/signin-google),並將使用一次性使用的code來獲取有關用戶的信息。最後,它將發起SignIn到ExternalCookie,併發起重定向到第1步中定義的RedirectUrl。
- 在RedirectUrl的controller action中,手動運行了ExternalCookie的Authenticaticate。這返回了一個包含谷歌的用戶信息的ClaimsPrincipal,最後,向MainCookie發起一個SignIn並將
ClaimsPrincipal
傳遞給它(如果需要的話,創建一個含有額外信息的新的ClaimsPrincipal
)。向ExternalCookie 發起SignOut,以便其Cookie被刪除。
OAuth2簡述
在上面的例子中,我們使用了一個client Id,一個client secret,一個 callback URL,我們簡單地提到Google的回應包含了一個“code”,但是我們並沒有用到所有這些信息。
這些都是OAuth2協議的術語,具體來說就是“授權碼工作流程”(你可以在這裡找到更全面的OAuth2說明)。
使用OAuth的第一步是註冊客戶端。在本文的例子中,客戶端是你的Web應用程式,你必須註冊,以便外部登錄提供程式具有關於它的信息。這些信息是必需的,以便在向用戶提交授權表單時,提供商以顯示應用程式的名稱,以及在用戶接受或拒絕應用程式的“要求”後知道將用戶重定向到哪裡。
在OAuth中,這些“requirements”被稱為“scopes”。 Google的兩個scopes“item”的示例是“profile”和“email”。
當你的應用程式將用戶重定向到Google並包含這些範圍時,系統會詢問用戶是否可以訪問profile和email信息。
總之,當你向外部登錄提供者註冊你的應用程式時,你必須為你的應用程式提供(至少)一個名字,並且提供一個回調url(e.g. www.mydomain.com/signin-google)。
然後你將得到一個客戶端ID和一個客戶端密鑰。客戶端ID和client密碼是你的Web應用程式開始使用外部登錄提供程式所需的全部東西。以下是用戶瀏覽器,Web應用程式和外部登錄提供程式之間的交互圖。這裡的術語我用的很隨意,實際的術語應該是授權伺服器,而實際上包含用戶帳戶的伺服器就是資源伺服器。他們可能是一樣的。如果你需要對這些術語進行更加嚴格的描述,你應該閱讀關於OAuth的 digitial ocean article about OAuth。
圖表:
這是授權碼授權。還有其他的工作流程,但是對於一個Web應用程式,這是你要使用的。這裡需要註意的重要的事情是,code只能被使用一次,client secret永遠不會發送到用戶的瀏覽器。這樣就很難讓人冒充你的Web應用程式。如果有人想冒充你的應用程式,那麼他們要拿到你的client secret ,為此,他們要能進入你的伺服器才行。
ASP.NET Identity 是怎麼做的?
當你使用Visual Studio創建一個新項目並選擇帶有成員資格和授權的Web應用程式,併為外部登錄提供程式添加一個身份驗證中間件時,你將得到類似於以下的啟動配置:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//...
app.UseIdentity();
app.UseGoogleAuthentication(new GoogleOptions
{
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "CLIENT_SECRET"
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
如果你看看UseIdentity擴展方法的源代碼,你會發現類似這樣的東西:
app.UseCookieAuthentication(identityOptions.Cookies.ExternalCookie);
app.UseCookieAuthentication(identityOptions.Cookies.TwoFactorRememberMeCookie);
app.UseCookieAuthentication(identityOptions.Cookies.TwoFactorUserIdCookie);
app.UseCookieAuthentication(identityOptions.Cookies.ApplicationCookie);
譯者註:在2.0中,由於Use系列方法被Add系列方法取代,所以這些代碼會發生變化。
這與我們之前描述的很相似。不同的是,有兩個新的外部認證中間件(TwoFactorRememberMeCookie和TwoFactorUserIdCookie 它們不在本文的討論範圍之內)以及“主要”認證中間件(具有AutomaticAuthenticate = true的中間件)和我們使用的存儲外部登錄提供程式認證結果(ExternalCookie)被交換(然而他們呢的執行順序不會受到影響)。
另外,GoogleAuthentication中間件配置了所有的預設選項。CallbackPath的預設值是 new PathString(“/ signin-google”),還做了一些事情來指定你使用的特定的外部登陸提供器中間件。
手動發起外部登陸提供器中間件的challenge被放在了 AccountController 的ExternalLogin 方法中。
public IActionResult ExternalLogin(string provider, string returnUrl = null)
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { ReturnUrl = returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
如果你要查看SignInManager中ConfigureExternalAuthenticationProperties的源代碼,你會發現它只是像我們前面的示例中那樣創建一個AuthenticationProperties實例:
public virtual AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null)
{
AuthenticationProperties authenticationProperties = new AuthenticationProperties()
{
RedirectUri = redirectUrl
};
authenticationProperties.Items["LoginProvider"] = provider;
return authenticationProperties;
}
稍後使用帶有“LoginProvider”的“item”。我會在適當的時候突出顯示它。
從AccountController的ExternalLogin action中可以看出,RedirectUri在AccountController上也被設置為ExternalLoginCallback action。讓我們看看這個action(我刪除了不相關的部分):
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
var info = await _signInManager.GetExternalLoginInfoAsync();
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
return RedirectToLocal(returnUrl);
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
}
}
第一行,var info = await _signInManager.GetExternalLoginInfoAsync();
在external cookie中間件中觸發一個Authentication 。但是返回的不是ClaimsPrincipal的實例,它將返回包含以下屬性的ExternalLoginInfo類的實例:
- Principal (
ClaimsPrincipal
) - LoginProvider
--- 這是從AuthenticationProperties的Items中讀取的。在描述challenge的時候,我曾經提到帶有“LoginProvider”鍵的item將會在以後被使用。這是使用它的地方。 - ProviderKey
--- 這是ClaimsPrincipal中的聲明http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
的值,你可以將其視為來自外部登錄提供程式的UserId
下一行var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
這將檢查AspNetUserLogins表中是否有記錄。此表將外部登錄提供程式和“provider key”(這是外部登錄提供程式的用戶標識)鏈接到AspNetUsers
表中的用戶(該表的主鍵是LoginProvider和ProviderKey的組合鍵) 。
下麵是該表中記錄的示例:
因此,如果你使用Google登錄,並且你的Google“用戶ID”為123123123123123123,並且你之前已將你的本地用戶(稍後會詳細介紹)與此外部登錄關聯,則ExternalLoginSignInAsync將向 主 Cookie中間件發出signIn並向外部cookie中間件發出SignOut。
當用戶第一次訪問時,AspNetUserLogins表中將不會有任何本地用戶或記錄,並且方法將簡單地返回SignInResult.Failed。然後將用戶重定向到ExternalLoginConfirmation頁面:
在這個頁面中,用戶會被要求確認他想用來創建本地帳戶的電子郵件(即AspNetUsers表中的記錄)。
當你單擊註冊按鈕時,你將被帶到AccountController中的ExternalLoginConfirmation action,這是它的簡化版本:
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null)
{
var info = await _signInManager.GetExternalLoginInfoAsync();
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
await _userManager.CreateAsync(user);
await _userManager.AddLoginAsync(user, info);
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
第一行:var info = await _signInManager.GetExternalLoginInfoAsync;
該行將獲取存儲在external Cookie中的信息並返回ExternalLoginInfo的實例。這與ExternalLoginCallback中完成的事完全相同。
第二行:var user = new ApplicationUser {UserName = model.Email,Email = model.Email};
該行使用在用戶單擊Register的頁面中輸入的電子郵件創建ASP.NET Identity用戶的新實例。
第三行在AspNetUsers表中創建一個新用戶: await _userManager.CreateAsync(user);
第四行: await _userManager.AddLoginAsync(user,info);
該行將新創建的用戶與我們剛纔使用的外部登錄提供程式相關聯。這意味著在AspNetUserLogins中創建一條新記錄。
此表中的記錄有四列,LoginProvider(info.LoginProvider,例如“Google”),ProviderKey(info.ProviderKey,例如123123123123,你可以認為它是剛剛登錄的用戶的Google用戶標識),ProviderDisplayName (至少在2017/04/29的ASP.NET Identity的這個版本中是這樣的),最後是UserId,它是第三行中新創建的用戶的用戶標識。
最後 await _signInManager.SignInAsync(user, isPersistent: false);
譯者註:最終的SignInAsync源碼是:
public virtual async Task SignInAsync(TUser user, AuthenticationProperties authenticationProperties, string authenticationMethod = null) { var userPrincipal = await CreateUserPrincipalAsync(user); // Review: should we guard against CreateUserPrincipal returning null? if (authenticationMethod != null) { userPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod)); } await Context.SignInAsync(IdentityConstants.ApplicationScheme, userPrincipal, authenticationProperties ?? new AuthenticationProperties()); }
為用戶創建一個ClaimsPrincipal並向application Cookie發出一個SignIn。這個application Cookie是AutomaticAuthenticate = true
的cookie,這意味著在下一個請求中,該中間件將設置HttpContext.User與cookie中編碼的用戶,有使用戶“登錄”。請註意,外部cookie從未在此流程中被刪除。這不是一個大問題,因為當用戶最終退出時,SignInManager.SignOutAsync被調用,並且在內部向所有認證中間件發起SignOut。
總結全文就是:如何在Asp.NetCore中使用外部登陸提供程式,包含只使用authentication中間件和與Identity共同使用。
使用ASP.NET Core Identity和外部登錄提供程式還有一些事情。你可以將其中多個外部登陸提供程式關聯到本地用戶帳戶。而且你可以將他們全部移除,如果你確定不會“shoot yourself on the foot”,例如移除所有用戶登錄的方式,不過這可能成為另一篇博文的話題。
譯者註:全文完
原文出自Rui Figueiredo的博文《External Login Providers in ASP.NET Core》
轉載請註明出處謝謝 :D