Identity Server 4是IdentityServer的最新版本,它是流行的OpenID Connect和OAuth Framework for .NET,為ASP.NET Core和.NET Core進行了更新和重新設計。在本文中,我們將快速了解IdentityServer 4存在的原因,然后直接進入並創建一個從零到英雄的工作實現。
IdentityServer 3與IdentityServer 4
目前流行的一句話是“概念上兼容”,但這對於Identity Server 4來說是正確的。概念是相同的,它仍然是按照規范構建的OpenID Connect提供程序,但是它的大部分內部和可擴展性點已經改變。當我們將客戶端應用程序與IdentityServer集成時,我們沒有集成到實現中。相反,我們使用OpenID Connect或OAuth規范進行集成。這意味着當前與IdentityServer 3一起使用的任何應用程序都將與IdentityServer 4一起使用。
Identity Server被設計為作為自托管組件運行,使用ASP.NET 4.x很難實現,而MVC仍然與IIS和System.Web緊密耦合,從而導致katana提供內部視圖引擎零件。借助在ASP.NET Core上運行的Identity Server 4,我們現在可以在ASP.NET Core可以運行的任何環境中使用任何UI技術和主機IdentityServer。這也意味着我們現在可以與現有的登錄表單/系統集成,從而實現升級。
IUserService
用於集成用戶存儲 的Identity Server 現在也已消失,取而代之的是以IProfileService
和形式的新用戶存儲抽象IResourceOwnerPasswordValidator
。
IdentityServer 3不會去任何地方,就像.NET Framework不會去任何地方一樣。就像微軟已將大多數活動開發轉移到.NET Core(參見Katana和ASP.NET Identity)一樣,我想IdentityServer最終會做同樣的事情,但我們在這里討論的是OSS,而項目保持這種狀態它始終是開放的PRs修復錯誤和相關的新功能。我不會很快放棄它,商業支持將繼續下去。
在寫作的初始階段,IdentityServer 4在RC5中,IdentityServer 3在v2.5.3上,計划在未來使用另一個主要版本(v3.0.0)。此文章已更新為IdentityServer 4 v2.0。
IdentityServer4以.NET標准2.0為目標,這意味着它可以針對.NET核心或.NET框架,盡管本文僅針對.NET Core。IdentityServer 4現在支持.NET Core 2.0,由於兩個版本之間的重大變化而留下.NET Core 1.x。
您可以在Dominick Baier的IdentityServer 4公告文章中閱讀有關IdentityServer 4背后原因的更多信息。
從.NET Core 2.0開始,還有一些重大變化。對於ASP.NET Core 1.x支持,請查看主存儲庫中的aspnetcore1分支。
在ASP.NET Core和.NET Core上實現IdentityServer4
對於我們的初始實現,我們將使用為演示和輕量級實現保留的內存服務。在本文的后面,我們將切換到實體框架,以更真實地表示IdentityServer的生產實例。
在開始本教程之前,請確保您使用的是最新版本的ASP.NET Core和.NET Core工具。在創建本教程時,我使用了.NET Core 2.0和Visual Studio 2017。
首先,我們需要一個使用.NET Core的新ASP.NET Core項目(在VS中參見'ASP.NET Core Web Application')。您將需要使用沒有身份驗證的Empty模板。確保您的項目設置為.NET Core和ASP.NET Core 2.0。
在開始編碼之前,將項目URL切換為HTTPS。在沒有TLS的情況下,您不應該運行身份驗證服務。假設您使用的是IIS Express,則可以通過打開項目屬性,進入“調試”選項卡並單擊“啟用SSL”來執行此操作。雖然我們在這里,但您應該將生成的HTTPS URL作為App URL,這樣當我們運行項目時,我們就會從正確的頁面開始。
如果在為localhost使用IIS Express開發證書時遇到證書信任問題,請嘗試按照本文中的步驟操作。如果您發現此方法存在問題,請隨意切換到自托管模式(而不是IIS Express,使用項目的命名空間運行)。
首先,我們需要安裝以下nuget包(目前為2.0.2編寫的文章):
IdentityServer4
現在到我們的Startup類開始注冊依賴項和連接服務。
在您的ConfigureServices
方法中添加以下內容以注冊所需的最低依賴項:
services.AddIdentityServer() .AddInMemoryClients(new List<Client>()) .AddInMemoryIdentityResources(new List<IdentityResource>()) .AddInMemoryApiResources(new List<ApiResource>()) .AddTestUsers(new List<TestUser>()) .AddDeveloperSigningCredential();
然后在您的Configure
方法中添加以下內容以將IdentityServer中間件添加到HTTP管道:
app.UseIdentityServer();
我們在這里做的是在我們的DI容器中注冊IdentityServer AddIdentityServer
,使用演示簽名證書AddDeveloperSigningCredential
,並為我們的客戶,資源和用戶使用內存存儲。通過使用,AddIdentityServer
我們還將所有生成的令牌/授權存儲在內存中。我們將很快添加實際的客戶,資源和用戶。
UseIdentityServer
允許IdentityServer開始攔截路由並處理請求。
我們實際上可以運行IdentityServer,它可能沒有UI,不支持任何范圍並且沒有用戶,但您已經可以開始使用它了!查看OpenID Connect Discovery文檔/.well-known/openid-configuration
。
OpenID Connect Discovery文檔
OpenID Connect Discovery文檔(被親切地稱為disco doc)可在此着名端點的每個OpenID Connect提供程序上使用(根據規范)。本文檔包含各種端點的位置(例如令牌端點和結束會話端點),提供程序支持的授權類型,可提供的范圍等信息。通過這個標准化文檔,我們開辟了自動集成的可能性。
您可以在OpenID Connect Discovery 1.0規范中閱讀有關OpenID Connect Discovery文檔的更多信息。
簽署證書
簽名證書是用於簽署令牌的專用證書,允許客戶端應用程序驗證令牌的內容在傳輸過程中未被更改。這涉及用於簽署令牌的私鑰和用於驗證簽名的公鑰。客戶端應用程序可以通過jwks_uri
OpenID Connect發現文檔訪問此公鑰。
當您創建並使用自己的簽名證書時,請隨意使用自簽名證書。此證書不需要由受信任的證書頒發機構頒發。
現在我們啟動並運行IdentityServer,讓我們添加一些數據。
客戶,資源和用戶
首先,我們需要存儲允許使用IdentityServer的客戶端應用程序,以及這些客戶端可以使用的資源以及允許對其進行身份驗證的用戶。
我們目前正在使用InMemory商店,這些商店接受他們各自實體的集合,我們現在可以使用一些靜態方法填充它們。
客戶端
IdentityServer需要知道允許哪些客戶端應用程序使用它。我想將此視為白名單,即您的訪問控制列表。然后將每個客戶端應用程序配置為僅允許執行某些操作,例如,他們只能請求將令牌返回到某些URL,或者他們只能請求某些信息。他們有訪問范圍。
internal class Clients { public static IEnumerable<Client> Get() { return new List<Client> { new Client { ClientId = "oauthClient", ClientName = "Example Client Credentials Client Application", AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = new List<Secret> { new Secret("superSecretPassword".Sha256())}, AllowedScopes = new List<string> {"customAPI.read"} } }; } }
這里我們添加一個使用Client Credentials OAuth授權類型的客戶端。此授權類型需要客戶端ID和客戶端密鑰來授權訪問,使用Identity Server提供的擴展方法簡單地對密碼進行哈希處理(畢竟我們從不在純文本中存儲任何密碼,這總比沒有好)。允許的范圍是允許此客戶端請求的范圍列表。這里我們的范圍是customAPI.read,我們現在將以API資源的形式初始化它。
資源和范圍
范圍代表您可以做的事情。它們代表我之前提到的范圍訪問。在IdentityServer 4中,作用域被建模為資源,它有兩種形式:Identity和API。標識資源允許您為將返回特定聲明集的作用域建模,而API資源作用域允許您建模對受保護資源(通常是API)的訪問。
internal class Resources { public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), new IdentityResource { Name = "role", UserClaims = new List<string> {"role"} } }; } public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource { Name = "customAPI", DisplayName = "Custom API", Description = "Custom API Access", UserClaims = new List<string> {"role"}, ApiSecrets = new List<Secret> {new Secret("scopeSecret".Sha256())}, Scopes = new List<Scope> { new Scope("customAPI.read"), new Scope("customAPI.write") } } }; } }
IdentityResources
前三個身份資源代表我們希望IdentityServer支持的一些標准OpenID Connect定義的范圍。例如,email
范圍允許返回email
和email_verified
聲明。我們還創建了一個自定義標識資源,其形式為經過身份驗證的用戶role
返回role
聲明。
快速提示,openid
使用OpenID Connect流時始終需要范圍。您可以在OpenID Connect規范中找到有關這些的更多信息。
ApiResources
對於api資源,我們正在建模一個我們希望保護的API customApi
。此API有兩個可以請求的范圍:customAPI.read
和customAPI.write
。
通過在這樣的范圍內設置聲明,我們確保將這些聲明類型添加到具有此范圍的任何標記中(當然,如果用戶具有該類型的值)。在這種情況下,我們確保將用戶角色聲明添加到具有此范圍的任何令牌。稍后將在令牌自省期間使用范圍秘密。
范圍與資源
OpenID Connect和OAuth作用域現在被建模為資源,是IdentityServer 3和IdentityServer 4之間最大的概念上的變化。
offline_access
現在,默認情況下支持用於請求刷新令牌 的作用域,並授權使用由該Client
屬性控制的此作用域AllowOfflineAccess
。
用戶
在完全成熟的用戶存儲(如ASP.NET Identity)的位置,我們可以使用TestUsers:
internal class Users { public static List<TestUser> Get() { return new List<TestUser> { new TestUser { SubjectId = "5BE86359-073C-434B-AD2D-A3932222DABE", Username = "scott", Password = "password", Claims = new List<Claim> { new Claim(JwtClaimTypes.Email, "scott@scottbrady91.com"), new Claim(JwtClaimTypes.Role, "admin") } } }; } }
用戶主題(或子)聲明是其唯一標識符。這應該是您的身份提供商獨有的東西,而不是電子郵件地址。我指出這是由於最近Azure AD的漏洞。
我們現在需要使用此信息更新我們的DI容器(而不是以前的空集合):
services.AddIdentityServer() .AddInMemoryClients(Clients.Get()) .AddInMemoryIdentityResources(Resources.GetIdentityResources()) .AddInMemoryApiResources(Resources.GetApiResources()) .AddTestUsers(Users.Get()) .AddDeveloperSigningCredential();
如果您再次運行此命令並再次訪問發現文檔,您現在將看到填充的部分scopes_supported
和claims_supported
部分。
OAuth功能
為了測試我們的實現,我們可以使用之前的OAuth客戶端從Identity Server獲取訪問令牌。這將使用Client Credentials流程,因此我們的請求將如下所示:
POST /connect/token
Headers:
Content-Type: application/x-www-form-urlencoded
Body:
grant_type=client_credentials&scope=customAPI.read&client_id=oauthClient&client_secret=superSecretPassword
這會將我們的訪問令牌作為JWT返回:
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0M2U4MjljMmI1NzQ4OTk2OTc1M2JhNGY4MjA1OTc5ZGYwZGE5ODhjNjQwY2ZmYTVmMWY0ZWRhMWI2ZTZhYTQiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE0ODE0NTE5MDMsImV4cCI6MTQ4MTQ1NTUwMywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAiLCJhdWQiOlsiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAvcmVzb3VyY2VzIiwiY3VzdG9tQVBJIl0sImNsaWVudF9pZCI6Im9hdXRoQ2xpZW50Iiwic2NvcGUiOlsiY3VzdG9tQVBJLnJlYWQiXX0.D50LeW9265IH695FlykBiWVkKDj-Gjiv-8q-YJl9qV3_jLkTFVeHUaCDuPfe1vd_XVxmx_CwIwmIGPXftKtEcjMiA5WvFB1ToafQ1AqUzRyDgugekWh-i8ODyZRped4SxrlI8OEMcbtTJNzhfDpyeYBiQh7HeQ6URn4eeHq3ePqbJSTPrqsYyG9YpU6azO7XJlNeq_Ml1KZms1lxrkXcETfo7U1h-z66TxpvH4qQRrRcNOY_kejq1x_GD3peWcoKPJ_f4Rbc4B-UvqicslKM44dLNoMDVw_gjKHRCUaaevFlzyS59pwv0UHFAuy4_wyp1uX7ciQOjUPyhl63ZEOX1w", "expires_in": 3600, "token_type": "Bearer"
如果我們將此訪問令牌轉到jwt.io,我們可以看到它包含以下聲明:
"alg": "RS256", "kid": "143e829c2b57489969753ba4f8205979df0da988c640cffa5f1f4eda1b6e6aa4", "typ": "JWT" "nbf": 1481451903, "exp": 1481455503, "iss": "https://localhost:44350", "aud": [ "https://localhost:44350/resources", "customAPI" ], "client_id": "oauthClient", "scope": [ "customAPI.read" ]
我們現在可以使用IdentityServer的令牌內省端點來驗證令牌,就好像我們是從外部方接收它的OAuth資源一樣。如果成功,我們將收到該標記中的聲明回復給我們。請注意,IdentityServer 4中的訪問令牌驗證端點在IdentityServer 4中不再可用。
在這里,我們之前創建的范圍秘密通過使用基本身份驗證來使用,其中用戶名是范圍Id,密碼是范圍秘密。
POST /connect/introspect
Headers:
Authorization: Basic Y3VzdG9tQVBJOnNjb3BlU2VjcmV0
Content-Type: application/x-www-form-urlencoded
Body:
token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0M2U4MjljMmI1NzQ4OTk2OTc1M2JhNGY4MjA1OTc5ZGYwZGE5ODhjNjQwY2ZmYTVmMWY0ZWRhMWI2ZTZhYTQiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE0ODE0NTE5MDMsImV4cCI6MTQ4MTQ1NTUwMywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAiLCJhdWQiOlsiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTAvcmVzb3VyY2VzIiwiY3VzdG9tQVBJIl0sImNsaWVudF9pZCI6Im9hdXRoQ2xpZW50Iiwic2NvcGUiOlsiY3VzdG9tQVBJLnJlYWQiXX0.D50LeW9265IH695FlykBiWVkKDj-Gjiv-8q-YJl9qV3_jLkTFVeHUaCDuPfe1vd_XVxmx_CwIwmIGPXftKtEcjMiA5WvFB1ToafQ1AqUzRyDgugekWh-i8ODyZRped4SxrlI8OEMcbtTJNzhfDpyeYBiQh7HeQ6URn4eeHq3ePqbJSTPrqsYyG9YpU6azO7XJlNeq_Ml1KZms1lxrkXcETfo7U1h-z66TxpvH4qQRrRcNOY_kejq1x_GD3peWcoKPJ_f4Rbc4B-UvqicslKM44dLNoMDVw_gjKHRCUaaevFlzyS59pwv0UHFAuy4_wyp1uX7ciQOjUPyhl63ZEOX1w
響應:
"nbf": 1481451903, "exp": 1481455503, "iss": "https://localhost:44350", "aud": [ "https://localhost:44350/resources", "customAPI" ], "client_id": "oauthClient", "scope": [ "customAPI.read" ], "active": true
如果您希望以編程方式執行此過程並以此方式授權訪問.NET Core資源,請查看IdentityServer4.AcessTokenValidation庫。
資源所有者密碼憑據(ROPC)授予類型
IdentityServer文檔還提供了有關如何使用資源所有者授權類型的指南。不要被這種授權類型包含用戶名和密碼的事實所迷惑,它仍然只是授權而不是身份驗證。實際上,文章和原始OAuth 2.0規范中有多個免責聲明,聲明此授權類型應僅用於舊版應用程序。請參閱我的文章為什么資源所有者密碼憑據授予類型不是身份驗證也不適合現代應用程序,以調查資源所有者授予類型的所有錯誤。
用戶界面
到目前為止,我們一直在沒有UI工作,讓我們通過從使用ASP.NET Core MVC的GitHub引入Quickstart UI來改變這一點。
要下載此文件,請將repo中的所有文件夾復制到項目中,或使用以下powershell命令(同樣,在項目文件夾中):
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.ps1'))
現在我們需要將ASP.NET MVC Core添加到我們的項目中。為此,首先將以下包添加到項目中(如果已安裝,則可以跳過此安裝Microsoft.AspNetCore.All
):
Microsoft.AspNetCore.Mvc Microsoft.AspNetCore.StaticFiles
然后添加到您的服務(ConfigureServices
):
services.AddMvc();
最后添加到HTTP管道的末尾(Configure
):
app.UseStaticFiles(); app.UseMvcWithDefaultRoute();
現在,當我們運行項目時,我們會看到一個閃屏。萬歲!現在我們有了UI,現在我們可以開始驗證用戶了。
IdentityServer 4快速入門UI啟動畫面
OpenID Connect
要使用OpenID Connect演示身份驗證,我們需要創建一個客戶端Web應用程序並在IdentityServer中添加相應的客戶端。
首先,我們需要在IdentityServer中添加一個新客戶端:
new Client { ClientId = "openIdConnectClient", ClientName = "Example Implicit Client Application", AllowedGrantTypes = GrantTypes.Implicit, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, "role", "customAPI.write" }, RedirectUris = new List<string> {"https://localhost:44330/signin-oidc"}, PostLogoutRedirectUris = new List<string> {"https://localhost:44330"} }
重定向和后注銷重定向uris的位置是我們即將推出的應用程序的URL。重定向uri需要路徑/signin-oidc
,這條路徑將由即將推出的中間件自動創建和處理。
這里我們使用OpenID Connect隱式授權類型。此授權類型允許我們通過瀏覽器請求身份和訪問令牌。我會稱之為最簡單的授權類型,但也是最不安全的。
客戶申請
現在我們需要創建客戶端應用程序。為此,我們需要另一個ASP.NET Core網站,這次使用Web應用程序(MVC)VS模板,但沒有認證。
要將OpenID Connect身份驗證添加到ASP.NET Core站點,我們需要將以下兩個包添加到我們的站點(同樣,如果您使用,可以跳過安裝Microsoft.AspNetCore.All
):
Microsoft.AspNetCore.Authentication.Cookies Microsoft.AspNetCore.Authentication.OpenIdConnect
然后在我們的DI(ConfigureServices
)中:
services.AddAuthentication(options => { options.DefaultScheme = "cookie"; }) .AddCookie("cookie");
在這里,我們告訴我們的應用程序使用cookie身份驗證,登錄用戶,並將其用作默認的身份驗證方法。雖然我們可能正在使用IdentityServer對用戶進行身份驗證,但每個客戶端應用程序仍需要發布自己的cookie(到其自己的域)。
現在我們需要添加OpenID Connect身份驗證:
services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("cookie") .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:44350/"; options.ClientId = "openIdConnectClient"; options.SignInScheme = "cookie"; });
在這里,我們告訴我們的應用程序使用我們的OpenID Connect Provider(IdentityServer),我們希望登錄的客戶端ID以及成功驗證時登錄的身份驗證類型(我們之前定義的cookie中間件)。
默認情況下,ID連接中間件選項將使用/signin-oidc
其重定向URI,請求范圍openid
和profile
,並用implicit
流動(只要求身份令牌)。
接下來我們需要在我們的管道(Configure
)之前添加身份驗證UseMvc
:
app.UseAuthentication();
現在剩下的就是讓頁面需要身份驗證才能訪問。讓我們將“添加”屬性添加到“聯系人”操作,因為聯系我們的人是我們想要的最后一件事。
[Authorize] public IActionResult Contact() { ... }
現在,當我們運行此應用程序並選擇“聯系”頁面時,我們將收到未經授權的401。這反過來將被我們的OpenID Connect中間件攔截,該中間件將302重定向到我們的Identity Server身份驗證端點以及必要的參數。
IdentityServer 4快速入門UI登錄屏幕
成功登錄后,IdentityServer將要求我們同意客戶端應用程序代表您訪問某些信息或資源(這些信息或資源對應於客戶端請求的身份和資源范圍)。可以在客戶端基於客戶端禁用此同意請求。默認情況下,ASP.NET Core的OpenID Connect中間件將請求openid和配置文件范圍。
IdentityServer 4快速入門UI同意屏幕
這就是使用隱式授權類型連接簡單OpenID Connect Client所需的全部內容。
Entity Framework Core
目前我們在內存存儲中使用,正如我們之前提到的那樣,它是用於演示目的,或者最多是非常輕量級的實現。理想情況下,我們希望將各種商店移動到一個持久性數據庫中,該數據庫在每次部署時都不會被刪除,或者需要更改代碼才能添加新條目。
IdentityServer有一個Entity Framework(EF)Core包,我們可以使用它來使用任何EF Core關系數據庫提供程序實現客戶端,范圍和持久授權存儲。
Identity Server Entity Framework Core軟件包已使用In-Memory,SQLite(內存中)和SQL Server數據庫提供程序進行了集成測試。如果您發現其他提供商存在任何問題或希望針對其他數據庫提供商編寫測試,請隨時在GitHub問題跟蹤器上打開問題或提交拉取請求)。
對於本文,我們將使用SQL服務器(SQL Express或本地數據庫會這樣做),因此我們需要以下nuget包:
IdentityServer4.EntityFramework Microsoft.EntityFrameworkCore.SqlServer
持久的贈款商店
持久授權存儲包含有關給定同意的所有信息(因此我們不會一直要求對每個請求的同意),引用令牌(存儲的jwt,其中只有與jwt相對應的密鑰被提供給請求者,使其易於撤銷),以及更多。如果沒有持久性存儲,則在每次重新部署IdentityServer時,令牌都將失效,並且我們無法一次承載多個安裝(無負載平衡)。
首先讓新的幾個變量:
const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=Test.IdentityServer4.EntityFramework;trusted_connection=yes;"; var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
然后,我們可以通過添加到AddIdentityServer
以下內容來添加對持久授權存儲的支持:
AddOperationalStore(options => options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
我們的遷移程序集是我們托管IdentityServer的項目。這對於不在您的托管項目中的DbContexts(在這種情況下它位於nuget包中)是必要的,並允許我們運行EF遷移。否則,我們將遇到一個例外情況,例如:
Your target project 'Project.Host' doesn't match your migrations assembly 'Project.BusinessLogic'. Either change your target project or change your migrations assembly. Change your migrations assembly by using DbContextOptionsBuilder. E.g. options.UseSqlServer(connection, b => b.MigrationsAssembly("Project.Host")). By default, the migrations assembly is the assembly containing the DbContext.
Change your target project to the migrations project by using the Package Manager Console's Default project drop-down list, or by executing "dotnet ef" from the directory containing the migrations project.
客戶端和Scope存儲
要為我們需要類似的東西,我們的更換范圍和客戶商店添加持久存儲AddInMemoryClients
,AddInMemoryIdentityResources
並AddInMemoryApiResources
用:
.AddConfigurationStore(options => options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
這些注冊還包括從我們的客戶端表中讀取的CORS策略服務。
運行EF遷移
要運行EF遷移,我們需要Microsoft.EntityFrameworkCore.Tools
在csproj中將包作為CLI工具添加:
<ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> </ItemGroup>
然后我們可以使用以下方法創建遷移:
dotnet ef migrations add InitialIdentityServerMigration -c PersistedGrantDbContext
dotnet ef migrations add InitialIdentityServerMigration -c ConfigurationDbContext
要使用我們之前使用的配置以編程方式創建客戶端和資源,請查看本文庫中的InitializeDbTestData方法。
ASP.NET Core Identity
為了為我們的用戶添加持久性存儲,Identity Server 4提供了ASP.NET Core Identity (ASP.NET Identity 3)庫的集成。我們將使用ASP.NET核心身份實體框架庫和基礎IdentityUser
實體再次使用SQL服務器執行此操作:
IdentityServer4.AspNetIdentity Microsoft.AspNetCore.Identity.EntityFrameworkCore Microsoft.EntityFrameworkCore.SqlServer
目前我們需要創建自己的自定義實現,IdentityDbContext
以覆蓋構造函數以獲取非泛型版本DbContextOptions
。這是因為IdentityDbContext
只有一個接受通用的構造函數DbContextOptions
,當我們注冊多個DbContext
s時,會導致無效的操作異常。我已經就此問題提出了一個問題,希望我們能盡快跳過這一步。
public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } }
然后,我們需要為我們的ConfigureServices
方法添加ASP.NET Identity DbContext的注冊。
services.AddDbContext<ApplicationDbContext>(builder => builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly))); services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>();
然后在我們的IdentityServerBuilder
替換AddTestUsers
中:
.AddAspNetIdentity<IdentityUser>()
我們再次需要運行遷移。這可以通過以下方式完成:
dotnet ef migrations add InitialIdentityServerMigration -c ApplicationDbContext
這就是將ASP.NET核心身份與IdentityServer 4連接起來所需的全部內容,但不幸的是,我們之前下載的Quickstart用戶界面不再正常工作,因為它仍在使用TestUserStore
。
但是,我們可以通過替換一些代碼,從Quickstart UI修改我們現有的AccountsController以適用於ASP.NET Core Identity。
首先,我們需要更改構造函數以接受ASP.NET核心標識UserManager
,而不是現有的TestUserStore
。我們的構造函數現在應該如下所示:
private readonly UserManager<IdentityUser> _userManager; private readonly IIdentityServerInteractionService _interaction; private readonly IEventService _events; private readonly AccountService _account; public AccountController( IIdentityServerInteractionService interaction, IClientStore clientStore, IHttpContextAccessor httpContextAccessor, IEventService events, UserManager<IdentityUser> userManager) { _userManager = userManager; _interaction = interaction; _events = events; _account = new AccountService(interaction, httpContextAccessor, clientStore); }
通過刪除TestUserStore
我們沒有破兩種方法:( Login
發布)和ExternalCallback
。我們可以Login
完全用以下方法替換該方法:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginInputModel model, string button) { if (button != "login") { var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); if (context != null) { await _interaction.GrantConsentAsync(context, ConsentResponse.Denied); return Redirect(model.ReturnUrl); } else { return Redirect("~/"); } } if (ModelState.IsValid) { var user = await _userManager.FindByNameAsync(model.Username); if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) { await _events.RaiseAsync( new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName)); AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; }; await HttpContext.SignInAsync(user.Id, user.UserName, props); if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } var vm = await _account.BuildLoginViewModelAsync(model); return View(vm); }
使用ExternalCallback
回調方法,我們需要使用以下內容替換find和provision邏輯:
[HttpGet] public async Task<IActionResult> ExternalLoginCallback() { var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme); if (result?.Succeeded != true) { throw new Exception("External authentication error"); } var externalUser = result.Principal; var claims = externalUser.Claims.ToList(); var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject); if (userIdClaim == null) { userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier); } if (userIdClaim == null) { throw new Exception("Unknown userid"); } claims.Remove(userIdClaim); var provider = result.Properties.Items["scheme"]; var userId = userIdClaim.Value; var user = await _userManager.FindByLoginAsync(provider, userId); if (user == null) { user = new IdentityUser { UserName = Guid.NewGuid().ToString() }; await _userManager.CreateAsync(user); await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, userId, provider)); } var additionalClaims = new List<Claim>(); var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); if (sid != null) { additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); } AuthenticationProperties props = null; var id_token = result.Properties.GetTokenValue("id_token"); if (id_token != null) { props = new AuthenticationProperties(); props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } }); } await _events.RaiseAsync(new UserLoginSuccessEvent