原文地址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