新文章 网摘 文章 随笔 日记

IdentityServer4:构建简单的令牌服务器并使用 JWT 保护您的 ASP.NET 核心 API

介绍

我收到了很多关于核心ApiBoilerplate ASP.NET 如何保护端点以及哪个URL验证令牌的问题。如果你不熟悉 ASP.NET Core,并且正在使用该模板开始使用,则可能会问自己同样的问题。我希望这篇文章能澄清一些困惑。API

我将尝试使这篇文章尽可能通用,以便即使您没有使用模板,您仍然可以应用相同的概念。ApiBoilerplate

开始

在 core 甚至传统 Web API ASP.NET 中,我们通常会使用 [Authorize] 属性来修饰我们的 或 ,以保护我们的 Api 端点免受匿名、未经授权或未经身份验证的用户的攻击:ControllersActions

控制器级别限制

[Authorize]
public class PersonController: ControllerBase
{
   //rest of the code here...
}

操作级别限制

[HttpGet]
[Authorize]
public async Task<IEnumerable<PersonResponse>> Get()
{
   //rest of the code here...
}

该属性为用户和角色提供筛选器,如果使用成员资格/标识提供者,则实现它相当容易。您甚至可以通过根据您的自定义需求扩展类来自定义其行为。有两种方法可以在 core ASP.NET 实现授权。其中包括基于角色的授权和基于策略的授权。从上面的示例中可以看出,它没有为属性指定任何参数,这意味着它只检查用户是否经过身份验证。对于本演示的目的来说,这已经足够了。[Authorize][AuthorizeAttribute][Authorize]

 

我们可以使用许多可能的选项来保护我们的 ,一些最受欢迎的选项正在使用 ,或者 。以下是一篇解释其差异的文章:API 密钥与 OAuth 令牌与 JSON 网络令牌APIsOAuth 2.0 Access TokensApi KeysJSON Web Tokens(JWT)

在此演示中,我们将用作访问令牌,其中包含经过签名和序列化的 JSON 数据负载。JWT

为什么选择智威汤逊?

因为它很简单,是用于验证API和服务器到服务器授权的绝佳技术。我不打算介绍细节,因为已经有很多关于此的指南,我个人推荐这个:JSON Web令牌简介JWT

请记住,a保证数据所有权,但不加密;您存储到 a 中的数据可以被拦截令牌的任何人看到,因为它只是序列化,而不是加密。换句话说,使用并不能使我们变得无懈可击。因此,始终与之齐头并进的额外保护层是通过连接来保护我们所有的网络流量。JWTJSONJWTJWTAPIJWTHTTPS

什么是身份服务器4?

标识服务器4 一个框架,为 ASP.NET 核心应用提供一组服务和中间件。我不会在这篇文章中介绍所有功能,我建议您前往官方文档页面,看看它提供了哪些功能。OpenID ConnectOAuth 2.0

身份服务器4支持多种协议流或授权类型,如、、和等。在这篇文章中,我们将看看流程。Authorization CodeClient CredentialsRefresh TokenImplicitClient Credentials

客户端凭据流

Client Credentials Flow是客户端应用使用 的过程,有时是 访问受保护资源以换取 。client_idclient_secretscopeaccess_token

此流是在未连接特定用户的情况下轻松保护 API 的推荐方法,大多数情况下,此方法在服务器到服务器方案中更好,因为系统中互连的内部应用程序需要进行身份验证,而无需登录 UI 来提供用户名和密码。

 

有关 IdentityServer 4 支持的授权类型的更多信息,请参阅:授权类型

但是,OAuth和开放ID连接(OIDC)到底是什么?

我在论坛和亲自认识的许多人都在询问这两个术语之间的区别:和。我想我会在这篇文章中简要概述它们,以帮助您更好地理解每个术语。OAuthOpenID

一般来说,是关于身份验证(证明你是谁(又名身份))。 是用户提供凭据的过程,通常以用户名和密码的形式提供,然后将其与存储在数据库、应用程序或资源中的凭据进行比较。 另一方面是关于授权(授予对文件/资源/数据的访问权限,而不必处理原始身份验证)。 是指确定用户在经过身份验证后允许执行的操作的过程。另一个叫做的东西两者兼而有之。OpenIDAuthenticationOAuthAuthorizationOpenID Connect

 

我们将使用 和 框架 来提供应用程序所需的大多数安全功能以及协议实现。这使第三方应用能够在服务器前提下获得对服务和 API 的有限访问权限。客户端不会使用资源所有者的凭据直接从我们的 API 访问受保护的资源,而是获取访问令牌。在 中,访问令牌通常是一个字符串,表示特定的范围、生存期和其他访问属性。.NETCore 3.1IdentityServer4OAuth 2.0OIDCHTTPOAuth 2.0

永远记住:

  • OAuth 2.0是指定如何传输令牌的授权协议。规范所需的令牌没有定义的结构,这意味着您可以根据需要生成字符串并实现令牌。
  • JWT定义令牌格式。
  • OAuth 2.0可用作令牌格式。这就是为什么我们将与 一起使用以获取访问令牌的原因。JWTJWTOAuth

为了给您一个快速概述,这里有一个术语表:OAuth

  • Resource Owner(又名用户) - 能够授予对受保护资源的访问权限的实体。
  • Resource Server(也称为 ASP.NET 核心 API)- 托管受保护资源的服务器,能够使用访问令牌接受和响应受保护的资源请求。
  • Client- 应用程序(桌面、Web、服务或移动应用程序)代表资源所有者并在其授权下发出受保护的资源请求。
  • Token Server- 在成功对资源所有者进行身份验证并获得授权后向客户端颁发访问令牌的服务。

我们不会在此示例中介绍,因为我们不会对允许用户输入其帐户凭据的用户身份验证执行任何操作。相反,我们将重点介绍使用客户端凭据授予类型的 API 到 API 身份验证和授权。Resource Owner

为了更清楚起见,下面是一个简单的图表,描述了每个组件如何相互交互的高级流程。

使用身份服务器构建令牌服务器4

让我们构建一个简单的用法,授权内部/外部客户端应用程序访问某个.您可以将其视为生成包含和/或信息的简单数据结构的系统。在这篇文章中,我们只介绍以授权访问的形式生成的方面。可以说,这将是“房子的钥匙”,让你穿过门口,进入受保护资源的住所,通常是一个 ASP.NET 核心Web API。一个好的做法是将此服务器作为单独的服务托管,而不是在特定于 Web 应用程序的应用中实现它。Token ServerIdentityServer4Resource ServerAuthorizationAuthenticationOAuthIdentityServer4access_tokenJWT

对于此特定演示,我将使用以下工具和框架:

  • 视觉工作室 2019
  • .NET Core 3.1
  • ASP.NET 核心 3.1
  • 身份服务器4
  • IdentityServer4.AccessToken验证
  • ApiBoilerPlate.AspNetCore

首先,让我们继续启动可视化工作室2019。使用空项目模板创建新的 ASP.NET 核心 Web 应用程序项目,并确保取消选中“身份验证”选项。

安装最新版本的 Nuget 包:IdentityServer4

PM> Install-Package IdentityServer4 -Version 3.1.1

注意:截至撰写本文时,最新版本为 3.1.1。

出于本演示的唯一目的,我们将仅使用内存中数据存储来保存客户端和资源的列表。在实际/生产应用程序中,应将这些数据存储在持久性数据存储区(如数据库)中。IndentityServer4 具有针对实体框架核心的持久层支持,您可以使用它来生成存储客户端、范围和授权所需的架构。

 

现在,让我们创建几个静态内部类,其中包含 和 的一些测试数据。clientsresources

配置接口资源

以下是我们的测试 API 资源的代码:


using IdentityServer4.Models;
using System.Collections.Generic;

namespace IdentityServer4Demo.TokenServer.Data
{
    internal static class ResourceManager
    {
        public static IEnumerable<ApiResource> Apis =>
            new List<ApiResource>
            {
                new ApiResource {
                    Name = "app.api.whatever",
                    DisplayName = "Whatever Apis",
                    ApiSecrets = { new Secret("a75a559d-1dab-4c65-9bc0-f8e590cb388d".Sha256()) },
                    Scopes = new List<Scope> {
                        new Scope("app.api.whatever.read"),
                        new Scope("app.api.whatever.write"),
                        new Scope("app.api.whatever.full")
                    }
                },
                new ApiResource("app.api.weather","Weather Apis")
            };
    }
}

该对象是位于命名空间中的类,并内置于标识服务器 4 中。这使我们能够模拟我们希望保护的API资源(又名 ASP.NET 核心/Web API)。从上面的代码示例中,我们正在对要保护的两个 API 进行建模:和 。ApiResourceIdentityServer4.Modelsapp.api.whateverapp.api.weather

 

请注意,每个对象的定义都不同。第一项使用不带参数的默认构造函数。如果要为每个 配置多个作用域,则可以使用此方法。在此示例中,我们定义了三个作用域:、 和 。ApiResourceAPIapp.api.whatever.readapp.api.whatever.writeapp.api.whatever.full

为方便起见,第二项使用构造函数参数。通常,对于每个 只需要一个作用域的较简单方案,通常可以将其用于。在这种情况下,该名称将自动成为资源的名称。APIapp.api.weatherscope

注意:这里要记住的几件事是,每个作用域都应该是唯一的,这就是为什么建议根据每个唯一名称定义作用域的原因。还可以在配置文件 () 中定义 ,然后将配置节传递给该方法。APIApiResourceJSONappsettings.jsonAddInMemoryApiResource

我们需要做的下一件事是设置一些测试客户端,我们希望授予对已配置的访问权限。ApiResources

配置客户端

以下是我们测试客户端的代码:


using IdentityServer4.Models;
using System.Collections.Generic;

namespace IdentityServer4Demo.TokenServer.Data
{
    internal static class ClientManager
    {
        public static IEnumerable<Client> Clients =>
            new List<Client>
            {
                    new Client
                    {
                         ClientName = "Client Application1",
                         ClientId = "t8agr5xKt4$3",
                         AllowedGrantTypes = GrantTypes.ClientCredentials,
                         ClientSecrets = { new Secret("eb300de4-add9-42f4-a3ac-abd3c60f1919".Sha256()) },
                         AllowedScopes = new List<string> { "app.api.whatever.read", "app.api.whatever.write" }
                    },
                    new Client
                    {
                         ClientName = "Client Application2",
                         ClientId = "3X=nNv?Sgu$S",
                         AllowedGrantTypes = GrantTypes.ClientCredentials,
                         ClientSecrets = { new Secret("1554db43-3015-47a8-a748-55bd76b6af48".Sha256()) },
                         AllowedScopes = { "app.api.weather" }
                    }
            };
    }
}

上面的代码只不过是一个简单的静态方法,它返回测试客户端。每个客户端都有唯一的 、 和 ,但都具有相同的设置为 。ListClientNameClientIdClientSecretsAllowedScopesAllowedGrantTypesGrantTypes.ClientCredentials

要记住的几件事:

  • 授权类型要求和授予访问权限。OAuth Client CredentialsClientIdClientSecrets
  • 此示例中的 使用随机字符串,该字符串使用标识服务器 4 中内置的扩展方法进行哈希处理。您可以根据自己的要求自由使用任何格式的机密。ClientIdSha256()
  • 此示例中的 使用也使用扩展方法进行哈希处理。同样,您可以根据自己的要求自由使用任何格式的机密。ClientSecretsUUIDSha256()
  • 客户端应用程序将使用 和 从 请求。ClientIdClientSecretsTokenIdentityServer4
  • 该属性定义允许某个客户端访问的作用域。在本例中,我们配置的 API 资源。作用域可用于根据读/写权限限制对资源的访问。AllowedScopes
  • 在作用域中建模为资源,有两种形式:和 。允许对将返回特定声明集的作用域进行建模,而作用域允许对受保护资源/API 的访问进行建模。我们不会在这篇文章中介绍标识资源。IdentityServer4IdentityAPIIdentity resourceAPI resource
  • 我们设置的值只是示例,您可能希望将这些值更改为所需的任何值。

配置身份服务器4

现在我们已经配置了内存中测试客户端和资源,接下来是启用 。好处是它提供了各种内存中配置,以便我们从配置的内存中对象轻松配置。APIIdentityServer4IdentityServer4IdentityServer

在文件的方法中添加以下代码:ConfigureServicesStartup.cs

 
public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryApiResources(Data.ResourceManager.Apis)
            .AddInMemoryClients(Data.ClientManager.Clients);

}

上面的代码在 中注册,它是 依赖项。IdentityServerDI Container

由于签名和验证令牌需要签名证书,因此我们使用 提供的 凭据自动生成签名凭据,以便在开发和原型设计期间测试功能。在实际应用程序中,应考虑改用并提供非对称密钥对和签名算法来对令牌进行签名和验证。AddDeveloperSigningCredential()IdentityServer4OAuthAddSigningCredential()

和 中间件为配置的客户端和 API 资源示例注册我们的内存中存储。AddInMemoryApiResources()AddInMemoryClients()

最后,在文件方法中添加以下代码:ConfigureStartup.cs

public void Configure(IApplicationBuilder app)
{
    app.UseIdentityServer();
}

就是这么简单!我们现在有一个简单的生成.Token ServerJWTs

请求令牌

IdentityServer4提供了一个发现终结点,该终结点可用于检索有关授权服务器的元数据,包括 。发现终结点可通过相对于 基址获得的。例如,如果我们在本地运行应用程序并向以下终结点执行请求:OIDCToken Endpoint/.well-known/openid-configurationToken ServerGET

https://localhost:44354/.well-known/openid-configuration
 

然后,我们将在下面看到以下模式:JSON

{
    "issuer": "https://localhost:44354",
    "jwks_uri": "https://localhost:44354/.well-known/openid-configuration/jwks",
    "authorization_endpoint": "https://localhost:44354/connect/authorize",
    "token_endpoint": "https://localhost:44354/connect/token",
    "userinfo_endpoint": "https://localhost:44354/connect/userinfo",
    "end_session_endpoint": "https://localhost:44354/connect/endsession",
    "check_session_iframe": "https://localhost:44354/connect/checksession",
    "revocation_endpoint": "https://localhost:44354/connect/revocation",
    "introspection_endpoint": "https://localhost:44354/connect/introspect",
    "device_authorization_endpoint": "https://localhost:44354/connect/deviceauthorization",
    "frontchannel_logout_supported": true,
    "frontchannel_logout_session_supported": true,
    "backchannel_logout_supported": true,
    "backchannel_logout_session_supported": true,
    "scopes_supported": [
        "app.api.whatever.read",
        "app.api.whatever.write",
        "app.api.whatever.full",
        "app.api.weather",
        "offline_access"
    ],
    "claims_supported": [],
    "grant_types_supported": [
        "authorization_code",
        "client_credentials",
        "refresh_token",
        "implicit",
        "urn:ietf:params:oauth:grant-type:device_code"
    ],
    "response_types_supported": [
        "code",
        "token",
        "id_token",
        "id_token token",
        "code id_token",
        "code token",
        "code id_token token"
    ],
    "response_modes_supported": [
        "form_post",
        "query",
        "fragment"
    ],
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "subject_types_supported": [
        "public"
    ],
    "code_challenge_methods_supported": [
        "plain",
        "S256"
    ],
    "request_parameter_supported": true
}
 

我们可以看到,可以在端点处访问。另请注意,我们之前配置的 、 和 作用域都添加到属性下。Token Endpointhttps://<YOUR-TOKENSERVER-DOMAIN>/connect/tokenapp.api.whatever.readapp.api.whatever.writeapp.api.whatever.fullapp.api.weatherscopes_supported

现在,让我们使用我们在 POSTMAN 中发出请求来配置的请求进行快速测试:JWTClientsPOSTHttp

POST /connect/token HTTP/1.1
Host: localhost:44354
Content-Type: application/x-www-form-urlencoded
client_id=t8agr5xKt4$3&
client_secret=eb300de4-add9-42f4-a3ac-abd3c60f1919&
grant_type=client_credentials&
scope=app.api.whatever.read app.api.whatever.write

我们应以以下格式呈现响应:HttpJSON

{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlJMUGVHQVhlQkluZFdsdVFwclBBdEEiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1ODE5MjM5MzQsImV4cCI6MTU4MTkyNzUzNCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTQiLCJhdWQiOiJhcHAuYXBpLndoYXRldmVyIiwiY2xpZW50X2lkIjoidDhhZ3I1eEt0NCQzIiwic2NvcGUiOlsiYXBwLmFwaS53aGF0ZXZlci5yZWFkIiwiYXBwLmFwaS53aGF0ZXZlci53cml0ZSJdfQ.lWQflS4xUe8hPcPHPjaTenOu7XkSWtsLHY4IGqLxu4AUtM0Ki8XSS2vEaLBfp5rIxgvjSMKbwv2SMJJCOPeB8Ck0L62ohldmAvs2fhiYYNNg4_Oz3ljVfbQz6zdP8xAVc6LKXXVM3Ed8GO_yRAgFDOOCfpiimj81h4QPd8yCrpWvHDihxsvwtCVGBQVQRMEv85fYhWoJ8qeX4sj2sW-dcuLzj-DFsPfqcX-BggXw5O4JVpmQ8QEUNCX1NCkLe8wYhu4GCAwTLK-umhirCSNzBhmAuIV3sXoPWa6VsYK4qJn8OVEOG0vDr8Jd394uauc6NYyxcZsf1qgxPa8-LRm_xA",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "app.api.whatever.read app.api.whatever.write"
}

属性中的值为 。要验证里面的内容和签名,我们可以使用名为 jwt.io 的在线工具来解码值。以下是解码后的元数据:access_tokenJWTJWT

标题:算法 & 令牌类型

{
  "alg": "RS256",
  "kid": "RLPeGAXeBIndWluQprPAtA",
  "typ": "at+jwt"
}

上面的结果包含一个公钥 Id () 和其他属性,客户端应用程序将使用这些属性来验证签名。客户端应用程序可通过发现文档中的终结点访问这些属性:kidJWT.well-known/openid-configuration/jwksOpenID Connect

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "RLPeGAXeBIndWluQprPAtA",
            "e": "AQAB",
            "n": "vkz4dwnAFRZ0KmoI4OXFgd53217md67N-egnTKlJQIF4buNjdpLuGzmivTS7_bkaJa4EnRk9O4LA2E59b_q-hDKV34XPpl5FEnr8SOmeNU2BhFDwKxVodqbw4ovkUH3pH5UOcCOIH-_jYBLxwFK2tsJitn_rfDx1bd_W0OnaFqrrgghYMZqf7EYRxCvqrWl6TURtY_zdvWJOWIWiwI7b8D39AxELGbWPpj9oP7sBFmeImqiPnJsnP3626aiB5FMBJeFKF0lwP86x9ZCSFDVMMQcJsrs-Fr6grWzGq-wR7FChfFhZulQ0vH610x323A491yEpYvyZRXjhEqgtOQRthQ",
            "alg": "RS256"
        }
    ]
}

上述声明是调用生成的签名凭据的一部分。生成的开发人员签名凭据将保留在名为 的文件中。默认情况下,该文件是在应用程序的根目录中创建的。存储用于对令牌进行签名的所需密钥,从而允许客户端应用程序验证令牌的内容在传输过程中是否未被更改。这涉及用于对令牌进行签名的私钥和用于验证签名的公钥。AddDeveloperSigningCredential()tempkey.rsatempkey.rsa

注意:您不必将文件签入到源代码管理/ git存储库中,如果该文件不存在,则将重新创建它。tempkey.rsa

有效负载:数据

{
  "nbf": 1581923934,
  "exp": 1581927534,
  "iss": "https://localhost:44354",
  "aud": "app.api.whatever",
  "client_id": "t8agr5xKt4$3",
  "scope": [
    "app.api.whatever.read",
    "app.api.whatever.write"
  ]
}

上述声明是 的基本元数据。JWT

  • nbf代表以前没有。
  • exp代表到期日。
  • iss代表发行人。
  • aud代表观众。需要客户端访问的资源名称。
  • client_id请求令牌的客户端应用程序的客户端 ID。
  • scope允许客户端访问的范围。

验证令牌

由于我们还没有可用于测试的 ASP.NET 核心 API 应用程序,因此我们可以利用 IdentityServer4 的令牌自检终结点来验证令牌。这模拟就好像我们是一个从外部客户端接收它的资源。OAuth

令牌自检在终结点上可用。在POSTMAN中,我们可以使用并将名称设置为用户名和密码。然后,这些值将被编码为字符串。最后,我们可以将 设置为令牌参数。下面是一个示例请求:https://localhost:5001/connect/introspectBasic AuthApiResourceApiSecretBase64JWTPOSTHttp

POST /connect/introspect HTTP/1.1
Host: localhost:5001
Content-Type: application/x-www-form-urlencoded
Authorization: Basic YXBwLmFwaS53aGF0ZXZlcjphNzVhNTU5ZC0xZGFiLTRjNjUtOWJjMC1mOGU1OTBjYjM4OGQ=
token={_INSERT_JWT_HERE_}

如果成功,我们将在回显给我们的给定令牌中收到以下声明:

{
    "nbf": 1581924913,
    "exp": 1581928513,
    "iss": "https://localhost:5001",
    "aud": "app.api.whatever",
    "client_id": "t8agr5xKt4$3",
    "active": true,
    "scope": "app.api.whatever.read app.api.whatever.write"
}

创建简单的 API 资源

让我们创建一个 ASP.NET 核心 API,它将充当令牌服务器的受众。将新项目添加到我们现有的解决方案中。创建新的 ASP.NET 核心 Web 应用程序项目,然后选择“API 项目模板”。

Visual Studio 对所有必要的文件和依赖项进行基架,以帮助你开始在 ASP.NET 核心中构建 RESTful API。生成的模板包括一个,用于模拟使用静态数据的简单请求,如下面的代码所示:WeatherForecastControllerGETHttp

namespace IdentityServer4Demo.WeatherApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", 
            "Cool", "Mild", "Warm", "Balmy", 
            "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

请注意,默认情况下,and 方法不受保护。这意味着我们应该能够通过调用请求来访问端点:ControllerAction/WeatherForecastGETHttp

让我们运行应用程序,以确保我们按预期工作。您应该会看到以下格式输出:APIJSON

[
    {
        "date": "2020-02-18T10:23:51.0316366-06:00",
        "temperatureC": 31,
        "temperatureF": 87,
        "summary": "Mild"
    },
    {
        "date": "2020-02-19T10:23:51.0316788-06:00",
        "temperatureC": 36,
        "temperatureF": 96,
        "summary": "Warm"
    },
    {
        "date": "2020-02-20T10:23:51.0316796-06:00",
        "temperatureC": 22,
        "temperatureF": 71,
        "summary": "Cool"
    },
    {
        "date": "2020-02-21T10:23:51.0316802-06:00",
        "temperatureC": -5,
        "temperatureF": 24,
        "summary": "Cool"
    },
    {
        "date": "2020-02-22T10:23:51.0316805-06:00",
        "temperatureC": -18,
        "temperatureF": 0,
        "summary": "Bracing"
    }
]

伟大!

使用智威汤逊保护接口

现在,让我们保护端点。一、安装:WeatherForecastIdentityServer4.AccessTokenValidation

PM> Install-Package IdentityServer4.AccessTokenValidation -Version 3.0.1

IdentityServer4.AccessTokenValidation是一个 ASP.NET 核心身份验证处理程序,用于验证和引用来自 IdentityServer4 的令牌。现在,让我们通过在文件方法中添加以下代码来设置具有 IdentityServer4 的身份验证处理程序:JWTJWTConfigureServicesStartup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = "https://localhost:44354";
                options.ApiName = "app.api.weather";
            });

    services.AddControllers();
}

上面的代码添加了使用“持有者”作为默认方案的身份验证支持。然后,它配置身份服务器身份验证处理程序。是托管您的身份服务器的基本 URL,在此示例中,我们的令牌服务器位于 。应在身份服务器中注册为 .默认情况下,该属性处于关闭状态,在生产中部署应用时应将其打开。Authorityhttps://localhost:44354ApiNameAudienceRequireHttpsMetadata

当我们的 API 使用 该属性进行修饰时,则发出请求的客户端应提供从 IdentityServer 生成的访问令牌,并将其作为 a 传递,然后才能被授予对终结点的访问权限。[Authorize]Bearer Authorization HeaderAPI

现在让我们回到 ,然后用属性装饰它:WeatherForecast Controller[Authorize]


[ApiController]
[Authorize]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    //...removed for brevity
}

最后,将中间件添加到 管道之间的 和 调用:UseAuthentication()UseRouting()UseAuthorization()

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //..removed for brevity

    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();

    //..removed for brevity
}

就是这么简单!我们现在有一个受计划保护的简单。API ResourceJWT Bearer Authentication

测试 API 终端节点

由于此演示由两个项目组成,并且我们的 ASP.NET Core API 应用程序依赖于我们的应用程序,因此我们需要在测试 时使 .通常,我们将在Web服务器中托管或部署这两个项目,以便能够在它们之间进行连接。幸运的是,自 Visual Studio 2017 以来,其中一项很酷的功能是启用多个启动项目。这意味着我们可以在可视化工作室中同时运行这两个应用程序。您需要做的就是:Token ServerToken ServerAPI Resource

  1. 右键单击解决方案
  2. 选择Set Startup Projects...
  3. 选择单选按钮Multiple Startup Projects
  4. 选择作为核心 API 和令牌服务器项目 ASP.NET 操作Start
  5. 单击,然后ApplyOK
  6. 现在生成并运行应用程序。

您将看到我们的端点现在返回错误,并表示我们现在正在访问该端点:/WeatherForecastGETHttp401Unauthorize

为了让我们获得授权,我们需要一个来自 我们的 持有人。让我们再次使用POSTMAN来请求一个,这次我们将使用凭据,因为我们的应用程序使用 作为我们之前已经配置的凭据。以下是获取的示例请求:JWTToken ServerJWTClient Application2WeatherForecastapp.api.weatherApiNameToken ServerPOSTHttpJWT

POST /connect/token HTTP/1.1
Host: localhost:44354
Content-Type: application/x-www-form-urlencoded
client_id=3X=nNv?Sgu$S&
client_secret=1554db43-3015-47a8-a748-55bd76b6af48&
grant_type=client_credentials&
scope=app.api.weather

响应应向我们提供包含在中的属性:JWTaccess_token

{
    "access_token": "_REDACTED_",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "app.api.weather"
}

复制 的值,然后在 POSTMAN 中创建一个新选项卡来测试我们的终结点。下面是带有标头的示例请求:access_token/WeatherForecastGETHttpBearer Authorization

GET /weatherforecast HTTP/1.1
Host: localhost:44380
Content-Type: application/json
Authorization: Bearer {_INSERT_JWT_HERE_}

现在,我们应该能够看到一个状态,其中包含来自调用的响应:Http200/weatherforecastGET

ApiBoilerPlate AccessTokenValidation Integration

如果您正在阅读这篇文章,并且尚未尝试使用构建 ASP.NET 核心API,请转到此处的官方存储库:https://github.com/proudmonkey/ApiBoilerPlateApiBoilerPlate

该模板还用于对访问令牌进行身份验证和验证。您可以在“文件”下找到配置 IdentityServer 身份验证的代码。下面是代码片段:IdentityServer4AccessTokenValidationInstallers/RegisterIdentityServerAuthentication.cs

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)  
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = config["ApiResourceBaseUrls:AuthServer"];
        options.RequireHttpsMetadata = false;
        options.ApiName = "api.boilerplate.core";
});

该模板会自动为您处理令牌请求。在基础结构/安装程序/注册Api资源.cs类的第 61 行中,您可以找到 作为 注入的 :OIDC Discover EndpointSingleton

services.AddSingleton<IDiscoveryCache>(r =>
{
    var factory = r.GetRequiredService<IHttpClientFactory>();
    return new DiscoveryCache(config["ApiResourceBaseUrls:AuthServer"], () => factory.CreateClient());
});

这意味着每次实例化对象时,都会自动绑定发现终结点,从而允许您直接调用令牌终结点,而无需重新配置它们。该对象提取在 appsettings.json 中配置的令牌服务器的基本 URL:HttpClientDiscoveryCache

"ApiResourceBaseUrls": {
    "AuthServer": "https://localhost:5000"
}

请记住,您需要将属性的值更改为令牌服务器的托管位置。AuthServer

请求客户端凭据令牌

您可以在服务/身份验证服务器连接.cs类中查看令牌请求的配置方式:

namespace ApiBoilerPlate.Services
{
    public class AuthServerConnect : IAuthServerConnect
    {
        private readonly HttpClient _httpClient;
        private readonly IDiscoveryCache _discoveryCache;
        private readonly ILogger<AuthServerConnect> _logger;
        private readonly IConfiguration _config;

        public AuthServerConnect(HttpClient httpClient, IConfiguration config, IDiscoveryCache discoveryCache, ILogger<AuthServerConnect> logger)
        {
            _httpClient = httpClient;
            _config = config;
            _discoveryCache = discoveryCache;
            _logger = logger;
        }
        public async Task<string> RequestClientCredentialsTokenAsync()
        {

            var endPointDiscovery = await _discoveryCache.GetAsync();
            if (endPointDiscovery.IsError)
            {
                _logger.Log(LogLevel.Error, $"ErrorType: {endPointDiscovery.ErrorType} Error: {endPointDiscovery.Error}");
                throw new HttpRequestException("Something went wrong while connecting to the AuthServer Token Endpoint.");
            }

            var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            {
                Address = endPointDiscovery.TokenEndpoint,
                ClientId = _config["Self:Id"],
                ClientSecret = _config["Self:Secret"],
                Scope = "SampleApiResource"
            });

            if (tokenResponse.IsError)
            {
                _logger.Log(LogLevel.Error, $"ErrorType: {tokenResponse.ErrorType} Error: {tokenResponse.Error}");
                throw new HttpRequestException("Something went wrong while requesting Token to the AuthServer.");
            }

            return tokenResponse.AccessToken;
        }
    }
}

上面的代码段通过传递已注册的 和令牌服务器,从 IndentityServer 令牌终结点请求访问令牌。和 还包含 appsettings.json 文件:DiscoveryCacheClientIdClientSecretScopeClientIdClientSecret

"Self": {
    "Id": "api.boilerplate.core",
    "Secret": "0a2e472b-f263-43fd-8372-3b13f5acf222"
}

提示:在生产或实际应用程序中,应考虑将机密和密钥存储在更安全的位置,如数据库或云保管库。

然后,每次通过 发出请求时,都会调用该方法。此过程封装在名为“受保护的资产持有者令牌”的自定义持有者令牌类中。下面是代码片段:RequestClientCredentialsTokenAsync()HttpHttpClientDelegatingHandler

namespace ApiBoilerPlate.Infrastructure.Handlers
{
    public class ProtectedApiBearerTokenHandler : DelegatingHandler
    {
        private readonly IAuthServerConnect _authServerConnect;

        public ProtectedApiBearerTokenHandler(IAuthServerConnect authServerConnect)
        {
            _authServerConnect = authServerConnect;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // request the access token
            var accessToken = await _authServerConnect.RequestClientCredentialsTokenAsync();

            // set the bearer token to the outgoing request as Authentication Header
            request.SetBearerToken(accessToken);

            // Proceed calling the inner handler, that will actually send the requestto our protected api
            return await base.SendAsync(request, cancellationToken);
        }
    }
}

然后,我们可以在中将 方法注册为服务:ProtectedApiBearerTokenHandlerTransientConfigureServicesStartup.cs

services.AddTransient<ProtectedApiBearerTokenHandler>();  

就是这样!我希望这篇文章对您有所帮助!

源码

此演示的源代码可在我的 GitHub 存储库中找到:https://github.com/proudmonkey/IdentityServer4Demo

引用

身份服务器4:构建简单的令牌服务器并使用 JWT 保护您的 ASP.NET 核心 API (vmsdurano.com)

posted @ 2022-10-22 13:36  岭南春  阅读(560)  评论(0)    收藏  举报