ASP.NET Core 使用外部登陸提供程序登陸的流程,以及身份認證的流程 (Challenge)


閱讀目錄


 

 

 

在Asp.Net Core 中使用外部登陸(google、微博...)


 

為了能夠使用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 。順便說一句,這就是讓用戶“登錄”的原因(在ASP.NET Core 2.X中,用戶登錄,其實就是調用HttpContext.SignInAsync,來登錄設置為DefaultAuthenticateScheme的cookie中間件,這會使得HttpContext.User.Identity.IsAuthenticated變為true,詳情可參考這里)。

 

如果你要使用 AutomaticAuthenticate = false 設置 cookie 中間件,並且在該cookie中間件的請求中有一個 authentication cookie,則用戶不會自動“登錄”(也就是說在ASP.NET Core 2.X中,調用HttpContext.SignInAsync,來登錄不為DefaultAuthenticateScheme的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方法傳任何參數Challenge()),然后具有AutomaticChallenge = true的認證中間件將是被選中的認證中間件,用戶將被定向到該認證中間件設置的登錄頁面。

在ASP.NET Core 2.X中還可以通過HttpContext來調用HttpContext.ChallengeAsync方法來發起一個challenge,這樣HttpResponse會被自動設置為302狀態,然后客戶端瀏覽器收到Http響應后會做相應的重定向跳轉,如果你在ASP.NET Core的中間件管道中想發起一個challenge,就可以采用這種方法。

注意

請不要去challenge一個不存在的scheme,如果調用HttpContext.ChallengeAsync方法時(或在Controller的Action中return Challengereturn new ChallengeResult時)傳入的scheme名字,在ASP.NET Core項目Startup類的ConfigureServices方法中都沒注冊,也就是說該scheme壓根就不存在,那么HttpContext.ChallengeAsync方法(return Challengereturn new ChallengeResult)會拋出異常,所以不要嘗試去challenge一個不存在的scheme。
例如如果代碼HttpContext.ChallengeAsync("ApplicationCookie")、return Challenge("ApplicationCookie")或return new ChallengeResult("ApplicationCookie")中的"ApplicationCookie"這個scheme不存在,那么就會引發異常

 

 

與認證中間件進行交互


 

Challenge只是可以在認證中間件上“執行(performed)”的操作之一。The others are Authenticate, SignIn and SignOut.
例如,如果你向身份驗證中間件“發起(issue)” 身份驗證(Authenticate )操作(假設此示例在controller action中):

ASP.NET Core 1.X

var claimsPrincipal = await context.Authentication.AuthenticateAsync("ApplicationCookie");

ASP.NET Core 2.X

AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync("ApplicationCookie"); var claimsPrincipal = authenticateResult.Principal;

譯者注:context.Authentication.AuthenticateAsync在2.0中已經過時,只需將其修改為context.AuthenticateAsync即可,不過返回值類型已經由 ClaimsPrincipal 變為 AuthenticateResult ,不過AuthenticateResult中含有 ClaimsPrincipal參考信息

 

注意

在上面 ASP.NET Core 2.X 的代碼中,如果HttpContext.AuthenticateAsync("ApplicationCookie")驗證scheme "ApplicationCookie"失敗,那么其返回的AuthenticateResult對象authenticateResult的Succeeded屬性(該屬性是bool類型)會為false,且authenticateResult的Principal屬性會為null。相反如果HttpContext.AuthenticateAsync("ApplicationCookie")驗證scheme "ApplicationCookie"成功,那么其返回的AuthenticateResult對象authenticateResult的Succeeded屬性(該屬性是bool類型)會為true

 

但是這里所述的HttpContext.AuthenticateAsync("ApplicationCookie")驗證scheme "ApplicationCookie"成功或失敗,是基於"ApplicationCookie"這個scheme存在的情況下得到的結果。如果HttpContext.AuthenticateAsync方法傳入的scheme名字,在ASP.NET Core項目Startup類的ConfigureServices方法中都沒注冊,也就是說該scheme壓根就不存在,那么HttpContext.AuthenticateAsync方法會拋出異常,所以不要嘗試用HttpContext.AuthenticateAsync去驗證一個不存在的scheme。

例如如果上面代碼HttpContext.AuthenticateAsync("ApplicationCookie")中的"ApplicationCookie"這個scheme不存在,那么就會引發異常

這將導致中間件嘗試認證並返回一個ClaimsPrincipal。例如,cookie中間件會在請求中查找cookie,並使用cookie中包含的信息構建 ClaimsPrincipal ClaimsIdentity


一般來講,如果給認證中間件配置了AutomaticAuthenticate = false ,那么你需要手動發起認證。
也可以發起(issue)SignIn:

ASP.NET Core 1.X

await context.Authentication.SignInAsync("ApplicationCookie", claimsPrincipal);

ASP.NET Core 2.X

await HttpContext.SignInAsync("ApplicationCookie", claimsPrincipal);

譯者注:這個也過時了,參考上一個

 

注意

在上面 ASP.NET Core 2.X 的代碼中,如果HttpContext.SignInAsync方法傳入的scheme名字,在ASP.NET Core項目Startup類的ConfigureServices方法中都沒注冊,也就是說該scheme壓根就不存在,那么HttpContext.SignInAsync方法會拋出異常,所以不要嘗試去HttpContext.SignInAsync一個不存在的scheme。

例如如果上面代碼HttpContext.SignInAsync("ApplicationCookie", claimsPrincipal)中的"ApplicationCookie"這個scheme不存在,那么就會引發異常

如果“ApplicationCookie”是一個cookie中間件,它將修改響應,以便在客戶端創建一個cookie。該cookie將包含重新創建作為參數傳遞的 ClaimsPrincipal 所需的所有信息。


最后,SignOut,例如,cookie中間件將刪除標識用戶的cookie。下面這段代碼展示了如何在名為“ApplicationCookie”的身份驗證中間件上調用注銷(sign out)的示例:

ASP.NET Core 1.X

await context.Authentication.SignOutAsync("ApplicationCookie"/*這里是中間件的AuthenticationScheme*/);

ASP.NET Core 2.X

await HttpContext.SignOutAsync("ApplicationCookie"/*這里是中間件的AuthenticationScheme*/);

譯者注:這個也過時了,參考上一個

 

注意
在上面 ASP.NET Core 2.X 的代碼中,如果HttpContext.SignOutAsync方法傳入的scheme名字,在ASP.NET Core項目Startup類的ConfigureServices方法中都沒注冊,也就是說該scheme壓根就不存在,那么HttpContext.SignOutAsync方法會拋出異常,所以不要嘗試去HttpContext.SignOutAsync一個不存在的scheme。
例如如果上面代碼HttpContext.SignOutAsync("ApplicationCookie")中的"ApplicationCookie"這個scheme不存在,那么就會引發異常。

但是如果"ApplicationCookie"這個scheme存在,只是現在"ApplicationCookie"並沒有被認證和登錄,那么調用HttpContext.SignOutAsync("ApplicationCookie")是不會拋出異常的,只是什么都不會發生而已。

 

 

中間件交互示例


 

如果沒有示例,那么很難想象這些東西是如何組合在一起的,接下來將展示一個使用cookie身份驗證中間件的簡單示例。

 

使用 cookie 認證中間件登陸用戶

以下是Cookie身份驗證和MVC中間件的設置:

ASP.NET Core 1.X

復制代碼
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 2.X

復制代碼
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(o => { o.DefaultScheme = "MyCookie"; o.DefaultChallengeScheme = "MyCookie"; }) .AddCookie("MyCookie", o => { o.LoginPath = new PathString("/account/login"); }); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
復制代碼

當一個請求到達配置了這個管道的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

ASP.NET Core 1.X

[Authorize(ActiveAuthenticationSchemes="MyCookie")] public IActionResult ActionThatRequiresAnAuthenticatedUser() { //... }

ASP.NET Core 2.X

[Authorize(AuthenticationSchemes = "MyCookie")] public IActionResult ActionThatRequiresAnAuthenticatedUser() { //... }

譯者注:ActiveAuthenticationSchemes已經過時,使用AuthenticationSchemes替換

為了涵蓋所有可能的方式來發出challenge,你也可以使用控制器中的Challenge方法:

public IActionResult TriggerChallenge() { return Challenge("MyCookie"); }

用這種方法手動發起challenge時需要注意一件重要事。如果你對身份驗證中間件(例如“MyCookie”)發出了一個challenge,然后身份驗證中間件“將用戶登入”(在這種情況下,請求中有一個對應這個中間件的cookie),那么中間件會將challenge作為響應未經授權的訪問,並將用戶重定向到/Account/AccessDenied。你可以通過在項目Startup類的ConfigureServices方法中,用CookieAuthenticationOptionsAccessDeniedPath屬性來更改該路徑,如下所示:

ASP.NET Core 2.X

復制代碼
services.AddAuthentication(o =>
{
    o.DefaultScheme = "MyCookie"; o.DefaultChallengeScheme = "MyCookie"; }) .AddCookie("MyCookie", o => { o.LoginPath = new PathString("/account/login"); o.AccessDeniedPath = new PathString("/Account/AccessDenied"); });
復制代碼

 

這背后的原因是,如果用戶已經登錄,並且向簽入該用戶的中間件發出challenge,則這意味着用戶沒有足夠的權限(例如,不具有所需的角色)。


以前版本的ASP.NET中的行為是將用戶重定向回登錄頁面。但是,如果使用外部登錄提供程序,則會造成問題。

 

外部登錄提供程序會“記住”你已經登錄。這就是為什么如果你已經登錄到Facebook,並且你使用了一個允許你登錄Facebook的網絡應用,你將被重定向到Facebook,然后立即返回到網絡應用(假設你已經授權在Facebook的網絡應用程序)。如果你沒有足夠的權限,可能會導致重定向循環。因此,在這些情況下,為了避免導致重定向循環,ASP.NET Core中的身份驗證中間件會將用戶重定向到拒絕訪問頁面。

 

 

使用外部登陸提供器中間件


 

依賴外部登錄提供程序時,最簡單的設置是配置一個cookie身份驗證中間件,負責對用戶進行登陸。然后再配置一個我們要使用的特定外部登錄提供程序的中間件。


如果我們想要使用Google登陸,我們可以像這樣配置我們的管道:

ASP.NET Core 1.X

復制代碼
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(); }
復制代碼

ASP.NET Core 2.X

復制代碼
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication("MainCookie") .AddCookie("MainCookie", o => { o.LoginPath = new PathString("/login"); }) .AddGoogle("Google", o => { o.ClientId = "YOUR_CLIENT_ID"; o.ClientSecret = "YOUR_CLIENT_SECRET"; o.CallbackPath = new PathString("/signin-google"); o.SignInScheme = "MainCookie"; }); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
復制代碼

 

譯者注:UseXyzAuthentication系列擴展方法已經過時,取而代之的是在ConfigService中的AddXyz()系列

每當有這個配置的請求進來,它將“通過”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。

關於SignInScheme

我們在上面的代碼中指定了Google認證中間件的SignInScheme為"MainCookie",它表示當用戶從Google的OAuth2登錄頁成功登錄並重定向回CallbackPath的URL地址后,Google認證中間件"Google"會使用Cookie認證中間件"MainCookie"作為載體,所以在隨后的所有Http請求中,如果我們使用方法HttpContext.AuthenticateAsync("MainCookie")和HttpContext.AuthenticateAsync("Google"),去驗證"MainCookie"和"Google"這兩個AuthenticationScheme,它們都會返回驗證成功處於登錄狀態。

 

然后我們使用方法HttpContext.SignOutAsync("MainCookie"),注銷"MainCookie"這個AuthenticationScheme后,我們再用方法HttpContext.AuthenticateAsync("MainCookie")和HttpContext.AuthenticateAsync("Google"),去驗證"MainCookie"和"Google"這兩個AuthenticationScheme,這時它們都會返回驗證失敗處於非登錄狀態了,這就是因為"MainCookie"這個AuthenticationScheme現在被注銷了,用戶瀏覽器中的Cookie被清除了,所以"Google"這個AuthenticationScheme現在也隨之被注銷了。

 

但是我們如果使用方法HttpContext.SignOutAsync("Google"),試圖只注銷"Google"這個AuthenticationScheme是沒有意義的,因為注銷后我們會發現用方法HttpContext.AuthenticateAsync("MainCookie")和HttpContext.AuthenticateAsync("Google"),去驗證"MainCookie"和"Google"這兩個AuthenticationScheme,它們還是會返回驗證成功都處於登錄狀態。這是因為此時Cookie認證中間件"MainCookie"並沒有被注銷,用戶瀏覽器中的Cookie還是存在的,而我們指定了Google認證中間件的SignInScheme為"MainCookie",所以相當於Google認證中間件"Google"是隨着Cookie認證中間件"MainCookie"的存在而存在,隨着Cookie認證中間件"MainCookie"的注銷而注銷。

 

外部登錄提供商中間件將“干預”請求的唯一情況是中間件被“challenged”或請求與CallbackPath匹配。


我們先來看看這個challenge。想象一下你有一個像這樣的controller action:

復制代碼
public IActionResult SignInWithGoogle() { var authenticationProperties = new AuthenticationProperties{ RedirectUri = Url.Action("Index", "Home") }; return Challenge(authenticationProperties, "Google"); }
復制代碼

當你發起challenge時,你可以指定AuthenticationProperties的一個實例。AuthenticationProperties類允許你指定用戶在成功驗證的情況下應該重定向到的其他選項(AuthenticationProperties類的RedirectUri屬性,既可以是以"/"開頭的絕對URI地址,例如:"/Home/Index",也可以是帶主機域名的完全URI地址,例如:"https://www.abc.com/Home/Index")。當發出這個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-google,那么在用戶成功通過Google身份驗證之后,他將被重定向到http://www.yourdomain.com/signin-google

 

當請求到來時,如果配置正確,它將匹配 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)。

注意

AuthenticationProperties中指定的重定向URL地址(在本例中是Home控制器中的Index aciton)在最初應該也是通過URL參數傳遞到Google的OAuth2登錄頁的,這樣用戶從Google登錄后,重定向回http://www.yourdomain.com/signin-google時,AuthenticationProperties中指定的重定向URL地址(在本例中是Home控制器中的Index aciton)又會作為URL參數一起傳回http://www.yourdomain.com/signin-google,然后GoogleAuthentication中間件才知道要再給客戶端瀏覽器發一個302重定向跳轉,才能最后跳轉到AuthenticationProperties中指定的重定向URL地址(在本例中是Home控制器中的Index aciton)。

 

在上面Action方法SignInWithGoogle中調用Challenge方法時,如果調用的是沒有AuthenticationProperties參數的重載return Challenge("Google"),那么其效果相當於是將當前執行Challenge方法時的Http請求URL,作為了AuthenticationPropertiesRedirectUri屬性,然后將該AuthenticationProperties傳入了Challenge方法,那么當用戶從Google登錄后,重定向回http://www.yourdomain.com/signin-google時,GoogleAuthentication中間件會再將客戶端瀏覽器重定向到執行return Challenge("Google")時的URL。這個行為可能看起來會很奇怪,但是仔細想想,大部分調用Challenge方法的原因都是ASP.NET Core中的Authorize攔截器攔截了某個URL,認為用戶沒有登錄或權限不足,從而調用了Challenge方法,那么當用戶從Google成功登錄后,ASP.NET Core會讓用戶再次嘗試訪問前面由Authorize攔截器攔截的URL。

 

 

使用額外的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。
下面是示例:

ASP.NET Core 1.X

復制代碼
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(); }
復制代碼

ASP.NET Core 2.X

復制代碼
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication("MainCookie") .AddCookie("MainCookie", o => { o.LoginPath = new PathString("/login"); }) .AddCookie("ExternalCookie", o => { }) .AddGoogle("Google", o => { o.ClientId = "YOUR_CLIENT_ID"; o.ClientSecret = "YOUR_CLIENT_SECRET"; o.CallbackPath = new PathString("/signin-google"); o.SignInScheme = "ExternalCookie"; }); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
復制代碼

 

譯者注:上述方法已經過時,參考1 參考2
主要變化在於AutomaticAuthenticateAutomaticChallenge被替代,因為這兩屬性的意圖其實只能用在一個中間件上,即只能讓一個認證中間件,自動觸發Authenticate 或者Challenge,所以他們移除了由 AddAuthentication(option) 指定,你可以先看這篇博客,因為不影響流程理解。

 

AutomaticAuthenticateAutomaticChallenge這兩個屬性在ASP.NET Core 2.X中,改到了在Startup類的ConfigureServices方法中用services.AddAuthentication方法的重載進行設置,如下所示:

復制代碼
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;//相當於在設置AutomaticAuthenticate options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;//相當於在設置AutomaticChallenge  }); services.AddMvc(); }
復制代碼

 

在ASP.NET Core 2.X中AuthenticationOptions的相關屬性說明如下:

  • DefaultScheme: if specified, all the other defaults will fallback to this value(也就是說當下面的這些Default...Scheme沒有被賦值時,就會采用DefaultScheme的值作為它們的默認值)
  • DefaultAuthenticateScheme: if specified, AuthenticateAsync() will use this scheme, and also the AuthenticationMiddleware added by UseAuthentication() will use this scheme to set context.User automatically. (Corresponds to AutomaticAuthentication)
  • DefaultChallengeScheme if specified, ChallengeAsync() will use this scheme, [Authorize] with policies that don't specify schemes will also use this
  • DefaultSignInScheme is used by SignInAsync() and also by all of the remote auth schemes like Google/Facebook/OIDC/OAuth, typically this would be set to a cookie.
  • DefaultSignOutScheme is used by SignOutAsync() falls back to DefaultSignInScheme
  • DefaultForbidScheme is used by ForbidAsync(), falls back to DefaultChallengeScheme

這和以前的情況唯一的區別是,現在有一個額外的身份驗證中間件(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 方法 :

ASP.NET Core 1.X

復制代碼
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("~/"); }
復制代碼

ASP.NET Core 2.X

復制代碼
public async Task<IActionResult> HandleExternalLogin() { AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync("ExternalCookie"); var claimsPrincipal = authenticateResult.Principal; //do something the the claimsPrincipal, possibly create a new one with additional information //create a local user, etc await HttpContext.SignInAsync("MainCookie", claimsPrincipal); await HttpContext.SignOutAsync("ExternalCookie"); return Redirect("~/"); }
復制代碼

譯者注:這里的代碼到了2.0時略有變化,參見之前的內容

 

注意

在上面Account controller中的 HandleExternalLogin 方法中,我們還應該首先加入代碼邏輯判斷ExternalCookie這個scheme是否已經被成功驗證(例如在上面ASP.NET Core 2.X代碼中判斷authenticateResult的Succeeded屬性是否為true,或者給HandleExternalLogin方法加上一個自定義邏輯的IAuthorizationFilter攔截器),因為只有當ExternalCookie這個scheme被成功驗證時,我們才允許執行Account controller中的 HandleExternalLogin 方法,否則應該將用戶重定向到登錄頁面,要求用戶先到Google的OAuth登錄頁面進行登錄。

我們在這個控制器動作中所做的是在ExternalCookie中間件中“手動”觸發一個Authenticate動作。這將返回從請求中的 cookie 重建的ClaimsPrincipal。由於我們已經設置了SignInScheme = ExternalCookie,所以在驗證成功之后,該cookie由 Google Authentication 中間件設置。GoogleAuthentication中間件在內部將執行類似以下的操作:

ASP.NET Core 1.X

HttpContext.Authentication.SignInAsync("ExternalCookie", claimsPrincipalWithInformationFromGoogle);

ASP.NET Core 2.X

HttpContext.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。

 

我們從用戶的視角來回顧一下:

  1. 用戶請求了一個action ,這個action向Google認證中間件發起challenge,例如, /Account/SignInWithGoogle。challenge action定義了RedirectUrl,例如/Account/HandleExternalLogin
  2. 響應將用戶瀏覽器重定向到Google的OAuth登錄頁面
  3. 成功驗證和授權Web應用程序后,Google會將用戶重定向回Web應用程序。例如/signin-google?code=…
  4. Google身份驗證中間件將接管請求(CallBackPath匹配/signin-google),並將使用一次性使用的code來獲取有關用戶的信息。最后,它將發起SignIn到ExternalCookie,並發起重定向到第1步中定義的RedirectUrl。
  5. 在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”,例如移除所有用戶登錄的方式,不過這可能成為另一篇博文的話題。

 

本文參考自:External Login Providers in ASP.NET Core


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM