原文地址html
Identity Server 4是IdentityServer的最新版本,它是流行的OpenID Connect和OAuth Framework for .NET,爲ASP.NET Core和.NET Core進行了更新和從新設計。在本文中,咱們將快速瞭解IdentityServer 4存在的緣由,而後直接進入並建立一個從零到英雄的工做實現。git
目前流行的一句話是「概念上兼容」,但這對於Identity Server 4來講是正確的。概念是相同的,它仍然是按照規範構建的OpenID Connect提供程序,可是它的大部份內部和可擴展性點已經改變。當咱們將客戶端應用程序與IdentityServer集成時,咱們沒有集成到實現中。相反,咱們使用OpenID Connect或OAuth規範進行集成。這意味着當前與IdentityServer 3一塊兒使用的任何應用程序都將與IdentityServer 4一塊兒使用。github
Identity Server被設計爲做爲自託管組件運行,使用ASP.NET 4.x很難實現,而MVC仍然與IIS和System.Web緊密耦合,從而致使katana提供內部視圖引擎零件。藉助在ASP.NET Core上運行的Identity Server 4,咱們如今能夠在ASP.NET Core能夠運行的任何環境中使用任何UI技術和主機IdentityServer。這也意味着咱們如今能夠與現有的登陸表單/系統集成,從而實現升級。sql
IUserService
用於集成用戶存儲 的Identity Server 如今也已消失,取而代之的是以IProfileService
和形式的新用戶存儲抽象IResourceOwnerPasswordValidator
。shell
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。express
IdentityServer4以.NET標準2.0爲目標,這意味着它能夠針對.NET核心或.NET框架,儘管本文僅針對.NET Core。IdentityServer 4如今支持.NET Core 2.0,因爲兩個版本之間的重大變化而留下.NET Core 1.x。編程
您能夠在Dominick Baier的IdentityServer 4公告文章中閱讀有關IdentityServer 4背後緣由的更多信息。json
從.NET Core 2.0開始,還有一些重大變化。對於ASP.NET Core 1.x支持,請查看主存儲庫中的aspnetcore1分支。api
對於咱們的初始實現,咱們將使用爲演示和輕量級實現保留的內存服務。在本文的後面,咱們將切換到實體框架,以更真實地表示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文檔(被親切地稱爲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") } } }; } }
前三個身份資源表明咱們但願IdentityServer支持的一些標準OpenID Connect定義的範圍。例如,email
範圍容許返回email
和email_verified
聲明。咱們還建立了一個自定義標識資源,其形式爲通過身份驗證的用戶role
返回role
聲明。
快速提示,openid
使用OpenID Connect流時始終須要範圍。您能夠在OpenID Connect規範中找到有關這些的更多信息。
對於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客戶端從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庫。
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演示身份驗證,咱們須要建立一個客戶端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所需的所有內容。
目前咱們在內存存儲中使用,正如咱們以前提到的那樣,它是用於演示目的,或者最可能是很是輕量級的實現。理想狀況下,咱們但願將各類商店移動到一個持久性數據庫中,該數據庫在每次部署時都不會被刪除,或者須要更改代碼才能添加新條目。
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.
要爲咱們須要相似的東西,咱們的更換範圍和客戶商店添加持久存儲AddInMemoryClients
,AddInMemoryIdentityResources
並AddInMemoryApiResources
用:
.AddConfigurationStore(options => options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
這些註冊還包括從咱們的客戶端表中讀取的CORS策略服務。
要運行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方法。
爲了爲咱們的用戶添加持久性存儲,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(provider, userId