原文出自 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");
譯者注:在2.0中已經過時,隻需将其修改為
context.Authentication.AuthenticateAsync
即可,不過傳回值類型已經由
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。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuEjNxYzN3gzYxEzMwIzYkRmY5AjZ0YDNkV2Y3QTNlZDMfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
如果請求中沒有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();
}
譯者注:上述方法已經過時, 參考1 參考2 主要變化在于
AutomaticAuthenticate
被替代,因為這輛屬性的意圖其實隻能用在一個中間件上,即隻能讓一個認證中間件,自動觸發Authenticate 或者Challenge,是以他們移除了由 AddAuthentication(option) 指定,你可以先看這篇部落格,因為不影響流程了解。
AutomaticChallenge
這和以前的情況唯一的差別是,現在有一個額外的身份驗證中間件(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
)。向ExternalCookie 發起SignOut,以便其Cookie被删除。ClaimsPrincipal
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中的聲明
的值,你可以将其視為來自外部登入提供程式的UserIdhttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
下一行
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”,例如移除所有使用者登入的方式,不過這可能成為另一篇博文的話題。
譯者注:全文完 轉載請注明出處謝謝 :D
歡迎大家加入.NetCore學習交流群 群号:180537383