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

来源:http://www.cnblogs.com/rocketRobin/archive/2017/12/16/8046259.html
-Advertisement-
Play Games

原文出自 "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中包含的信息構建 ClaimsPrincipalClaimsIdentity

一般來講,如果給認證中間件配置了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身份驗證和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();
}

譯者註:上述方法已經過時,參考1 參考2

主要變化在於AutomaticAuthenticateAutomaticChallenge被替代,因為這輛屬性的意圖其實只能用在一個中間件上,即只能讓一個認證中間件,自動觸發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。

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

  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”,例如移除所有用戶登錄的方式,不過這可能成為另一篇博文的話題。

譯者註:全文完

原文出自Rui Figueiredo的博文《External Login Providers in ASP.NET Core》

轉載請註明出處謝謝 :D


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 列表生成式即List Comprehensions,是Python內置的非常簡單卻強大的可以用來創建list的生成式。運用列表生成式,可以寫出非常簡潔的代碼。 ...
  • *過程描述 當瀏覽器發出一個http請求後,該請求被UrlRoutingModule截獲,UrlRoutingModule根據請求上下文去系統路由表(RouteTable)中匹配,從中獲取一個RouteData,包含了請求的路由信息,特別是包含一個RouteHandler屬性,因為在global的A ...
  • 使用表達式目錄樹實現兩個不同類型的屬性賦值: public class People { public int Age { get; set; } public string Name { get; set; } public int Id; } public class PeopleCopy { ...
  • 今天把近期發現的各種軟體問題做了修改,併發布新版ShoneSharp.13.6.exe,最新的網盤鏈接為:https://pan.baidu.com/s/1nv1hmJn ...
  • 業務層介面 public static string DataTableToBaseIService(string tableName, string nameSpace, string className) { var table = SqlTableHelper.GetSQLTableInfo( ...
  • 功能:將文件上傳到七牛雲存儲 準備工作 註冊七牛賬號,提交實名認證(基本上1天內內審核通過) 登錄七牛後臺->對象存儲->新建空間 (基本概念:https://developer.qiniu.com/kodo/manual/3978/the-basic-concept) 新建空間後會有免費功能變數名稱可以使 ...
  • 一、前言 本篇開發環境?1、操作系統: Windows 10 X642、SDK: .NET Core 2.0 Preview 二、安裝 .NET Core SDK 1、下載 .NET Core下載地址:https://www.microsoft.com/net/download/core根據自己電腦 ...
  • 根據自己項目需求編寫,僅供參考 個人建議:使用Excel模板進行導出操作。儘量避免自己生成Excel(既繁瑣又容易出BUG)。大多情況下導出Excel都是固定格式,使用模板導出會方便很多。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...