ASP.NET Core的身份认证框架IdentityServer4--入门【转】

原文地址

Identity Server 4是IdentityServer的最新版本,它是流行的OpenID Connect和OAuth Framework for .NET,为ASP.NET Core和.NET Core进行了更新和重新设计。在本文中,我们将快速了解IdentityServer 4存在的原因,然后直接进入并创建一个从零到英雄的工作实现。

IdentityServer 3与IdentityServer 4

IdentityServer

目前流行的一句话是“概念上兼容”,但这对于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(参见KatanaASP.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 BaierIdentityServer 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_uriOpenID 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范围允许返回emailemail_verified声明。我们还创建了一个自定义标识资源,其形式为经过身份验证的用户role返回role声明。

快速提示,openid使用OpenID Connect流时始终需要范围。您可以在OpenID Connect规范中找到有关这些的更多信息。

ApiResources

对于api资源,我们正在建模一个我们希望保护的API customApi。此API有两个可以请求的范围:customAPI.readcustomAPI.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_supportedclaims_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启动画面

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,请求范围openidprofile,并用implicit流动(只要求身份令牌)。

接下来我们需要在我们的管道(Configure)之前添加身份验证UseMvc

app.UseAuthentication();

现在剩下的就是让页面需要身份验证才能访问。让我们将“添加”属性添加到“联系人”操作,因为联系我们的人是我们想要的最后一件事。

[Authorize]
public IActionResult Contact() { ... }

现在,当我们运行此应用程序并选择“联系”页面时,我们将收到未经授权的401。这反过来将被我们的OpenID Connect中间件拦截,该中间件将302重定向到我们的Identity Server身份验证端点以及必要的参数。

IdentityServer 4快速入门UI登录屏幕

IdentityServer 4快速入门UI登录屏幕

成功登录后,IdentityServer将要求我们同意客户端应用程序代表您访问某些信息或资源(这些信息或资源对应于客户端请求的身份和资源范围)。可以在客户端基于客户端禁用此同意请求。默认情况下,ASP.NET Core的OpenID Connect中间件将请求openid和配置文件范围。

IdentityServer 4快速入门UI同意屏幕

IdentityServer 4快速入门UI同意屏幕

这就是使用隐式授权类型连接简单OpenID Connect Client所需的全部内容。

Entity Framework Core

目前我们在内存存储中使用,正如我们之前提到的那样,它是用于演示目的,或者最多是非常轻量级的实现。理想情况下,我们希望将各种商店移动到一个持久性数据库中,该数据库在每次部署时都不会被删除,或者需要更改代码才能添加新条目。

IdentityServer有一个Entity Framework(EF)Cor​​e包,我们可以使用它来使用任何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存储

要为我们需要类似的东西,我们的更换范围和客户商店添加持久存储AddInMemoryClientsAddInMemoryIdentityResourcesAddInMemoryApiResources用:

.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,当我们注册多个DbContexts时,会导致无效的操作异常。我已经就此问题提出了一个问题,希望我们能尽快跳过这一步。

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, user.Id, user.UserName));
    await HttpContext.SignInAsync(
        user.Id, user.UserName, provider, props, additionalClaims.ToArray());

    await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

    var returnUrl = result.Properties.Items["returnUrl"];
    if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl)) {
        return Redirect(returnUrl);
    }

    return Redirect("~/");
}
posted @ 2018-08-10 17:30 MPotato 阅读(...) 评论(...) 编辑 收藏