使用 GraphQL 数据访问和 JWT 身份验证的 .NET 5 服务





5.00/5 (9投票s)
实现 GraphQL 技术、优化存储库访问、JSON Web Token (JWT) 身份验证以及其他一些有用功能的 Web 服务。
目录
引言
本文介绍了两个 .NET 5 Web 服务。第一个 GraphQlService
使用 GraphQL 技术支持与数据库(SQL Server)进行创建、检索、更新和删除 (CRUD) 操作。传输层安全 (TLS) 可保护消息在网络传输过程中不被读取,并使用 JSON Web Token (JWT) 进行用户身份验证和授权。第二个 LoginService
提供用户登录机制,并根据用户的凭据生成 JWT。
Wiki:
GraphQL 是一种用于 API 的开源数据查询和操作语言,以及用于使用现有数据满足查询的运行时。GraphQL 由 Facebook 于 2012 年内部开发,并于 2015 年公开发布。目前,GraphQL 项目由 GraphQL 基金会管理。它提供了一种开发 Web API 的方法,并已与 REST 及其他 Web 服务架构进行过比较和对比。它允许客户端定义所需数据的结构,并且服务器返回的数据结构与请求相同,因此可以防止返回过量数据。
本文的代码演示了以下主要功能:
- 使用 GraphQL 技术对事务性数据存储库进行 CRUD 操作
- 现成的、方便的 Playground 和 GraphiQL Web UI 应用程序,用于 GraphQL 查询和突变,无需前端代码
- JWT 身份验证
- OpenAPI(又名 Swagger)与 GraphQL 结合使用
- 灵活的可配置日志记录(目前仅配置为向控制台输出最少信息)
- 使用内存中服务进行集成测试
使用了 NuGet 中提供的几个用于 GraphQL 开发的开源包。
服务如何工作?
服务的工作流程如下图所示
为了开始工作,用户向 LoginService
(1) 提供凭据(用户名和密码)。后者生成 JWT 并将其返回给用户。然后用户将查询/更新发送到 GraphQlService
(2) 并从服务接收响应。
这些服务有单独的数据库。LoginService
访问的用户数据库 UsersDb
包含一个 Users
表,该表包含每个用户的用户名、密码(实际应用中是加密的)和角色。GraphQlService
访问的 Person 数据库 PersonsDb
包含几个与人员、组织及其关系和隶属关系相关的表。
GraphQL 的使用和优化
GraphQL 定义了客户端和服务器之间关于数据检索(查询)和更新(突变)的契约。查询和突变都构成类似 JSON 的结构。检索到的数据以与请求非常相似的结构格式化并返回给客户端。由于 GraphQL 查询的层次结构,数据检索过程是一个对嵌套字段处理程序调用的序列。
GraphQL 暗示对每个数据字段使用解析器函数(resolvers)。通常,GraphQL 的实现(包括此处使用的实现)在形成 Web 响应层次结构期间确保调用适当的解析器。如果每个解析器都向数据库发出一个 SELECT
查询,那么这些查询的总数将等于层次结构中每个级别的返回行数。考虑一个获取所有人员的查询。在最顶层,获取所有 n 个人员。然后,在第二层,执行 n 次 SELECT
查询以获取每个人员的隶属关系和关系。在后续的每个级别都会观察到类似的情况。显然,大量的返回行会导致大量的数据库查询,从而导致严重的性能损失。此情况下的数据库查询数量为:
levels
这个问题通常被称为 N + 1 查询问题。
高效的 GraphQL 实现必须为此问题提供合理的解决方案。本工作中实现的解决方案可以表述如下。在“朴素”实现中,每个字段的处理程序都会调用数据库来检索数据。在优化解决方案中,每个级别上字段处理程序的第一次调用将从数据库检索该级别所有字段的数据,并将其存储在附加到 GraphQL 上下文对象的缓存中。GraphQL 上下文对象可供所有字段处理程序使用。同一级别字段处理程序的后续调用将从缓存中获取数据,而不是从数据库中获取。在优化情况下,数据库查询数量为:
database_queries = levels
您可以看到,数据库调用(SELECT)的数量对应于 GraphQL 查询的内部级别数,并且与每个级别上获取的记录数无关。
图 2 和图 3 所示说明了这种差异
解析器的返回值将自动插入到 GraphQL 查询的响应对象中。我们只需要在当前级别的解析器中提供已从数据库中获取的数据的返回值。最简单的方法是将获取的数据放入内存中的缓存对象,并将其附加到可供所有解析器使用的上下文对象。缓存组织为一个字典,其键根据解析器命名。每个解析器从缓存中提取一个具有相应键的数据片段并返回。
这些返回值将构成在 GraphQL 查询处理结束时发送回客户端的响应对象。由于缓存对象是上下文对象的属性,因此它将与上下文对象一起在 GraphQL 查询处理结束时被销毁。因此,缓存对象是为每个客户端请求创建的,其生命周期仅限于该请求的处理。
基于内存缓存的数据获取优化可以提高性能。但其限制在于可用运行内存(RAM)的大小。如果缓存对象太大,无法容纳在单个进程内存中,则可以使用分布式缓存解决方案,例如 Redis、Memcached 或类似的解决方案。在本文中,我们假设使用简单的内存缓存,这足以满足绝大多数实际情况。
组件和结构
组件 (Component) | 项目类型 | Location | 描述 |
GraphQlService | 服务 (控制台应用程序) | .\ | 该服务使用 GraphQL 技术执行 CRUD 操作。它提供两个控制器。GqlController 处理所有 GraphQL 请求,而 PersonController 处理无参数的 GET 请求,返回一些预定义文本,以及另一个带有 Person id 作为参数的 GET 请求。此请求在内部被视为一个普通 GraphQL 请求,带有硬编码的查询。它充当常用 GraphQL 查询的“快捷方式”。在此,PersonController 主要起说明作用。 |
LoginService | 服务 (控制台应用程序) | .\ | 该服务支持用户登录过程。它有一个 LoginController ,在响应用户凭据时创建 JWT。 |
ServicesTest | 测试项目 (控制台应用程序) | .\Tests | 该项目为两个服务提供集成测试。这些测试基于内存中服务(in-memory service)的概念。这种方法使开发人员可以轻松地测试实际的服务代码。 |
ConsoleClient | 控制台应用程序 | .\ | 一个简单的客户端控制台应用程序,用于服务。 |
PersonModelLib | DLL | .\Model | 该项目为给定的域问题(在本例中为 Persons )提供特定代码。 |
AsyncLockLib | DLL | .\Libs | 为 async/await 方法提供锁定机制,特别用于实现 GraphQL 缓存。 |
AuthRolesLib | DLL | .\Libs | 提供 enum UserAuthRole 。 |
GraphQlHelperLib | DLL | .\Libs | 包含通用的 GraphQL 相关代码,包括用于数据缓存以解决 N + 1 查询问题的代码。 |
HttpClientLib | DLL | .\Libs | 用于创建 HTTP 客户端,实现 HttpClientWrapper 类。 |
JwtAuthLib | DLL | .\Libs | 根据用户凭据生成 JWT。 |
JwtLoginLib | DLL | .\Libs | 提供用户登录处理,使用 JwtAuthLib 。 |
RepoInterfaceLib | DLL | .\Libs | 定义 IRepo<T> 接口,用于处理事务性数据存储库。 |
RepoLib | DLL | .\Libs | 实现 RepoInterfacesLib 中的 IRepo<T> 接口,用于 EntityFrameworkCore 。它为数据保存过程提供事务支持。 |
如何运行?
先决条件(适用于 Windows)
- 本地 SQL Server(请参见服务 appsettings.json 文件中的连接字符串)
- 支持 .NET 5 的 Visual Studio 2019 (VS2019)
- 用于测试带身份验证的用例的 Postman 应用程序
操作顺序
-
使用 VS2019 打开 GraphQL_DotNet.sln 解决方案,该解决方案支持 .NET 5,并构建解决方案。
-
使用 SQL Server。为简化起见,采用了 Code First 范例。在运行相应的服务或其集成测试时,会自动创建
UsersDb
和PersonsDb
数据库。如果需要,请调整 appsettings.json 服务配置文件中的连接字符串。启动时,数据库会从代码中填充一些初始记录。为了确保标识机制的正常运行,所有这些记录都被分配了负数 Id,除了UsersDb.Users
,因为该表在此工作中不会被程序化更改。 GraphQlService
的配置文件 appsetting.json 包含FeatureToggles
对象。"FeatureToggles": { "IsAuthJwt": true, "IsOpenApiSwagger": true, "IsGraphIql": true, "IsGraphQLPlayground": true, "IsGraphQLSchema": true }
默认情况下,所有选项都设置为
true
。让我们先开始不进行身份验证,并将"IsAuthJwt"
设置为false
。-
启动
GraphQlService
。可以从 VS2019 以服务形式或在 IIS Express 下启动。带有 GraphQL Playground Web UI 应用程序的浏览器会自动启动。在
Playground
网页中,您可以查看 GraphQL 架构,并尝试不同的查询和突变。一些预定义的查询和突变可以从文件 queries-mutations-examples.txt 中复制。图 4. Playground Web 应用程序。您可以使用类似的 GraphiQL Web 应用程序代替
Playground
:浏览到 https://:5001/graphiql。图 5. GraphiQL Web 应用程序。 -
Playground 应用程序使用中间件直接获取响应,绕过
GqlController
(它主要在开发过程中使用,但在本项目中,在所有版本中都可用)。它不调用GqlController
,而GqlController
是生产环境中供客户端使用的。要使用GqlController
,您可以使用 Postman 应用程序。在 Postman 中,向 https://:5001/gql 发送一个 POST 请求,在
Body
->GraphQL
中,在 QUERY 文本框中输入您的实际 GraphQL 查询/突变。图 6. 使用 Postman 进行 GraphQL 查询。 -
您也可以使用
OpenApi
(又名 Swagger):浏览到 https://:5001/swagger。图 7. OpenAPI(Swagger)。在 Swagger 网页中,激活 POST /Gql。
然后在 Postman 中,点击右上角的 Code 链接。
图 8. Postman 的 HTTP 请求。将查询复制到 Swagger 的
Request body
文本框并执行方法。图 9. POST /Gql 请求。图 10. POST /Gql 响应。 -
在所有情况下,您都可以使用不安全的 HTTP 调用 https://:5000,这允许用于说明和调试。
-
现在,让我们使用 JWT 身份验证。停止运行
GraphQlService
(如果正在运行),在GraphQlService
的配置文件 appsetting.json 的FeatureToggle
对象中,将"IsAuthJwt"
设置为true
,在 VS2019 中,将LoginService
和GraphQlService
定义为 Multiple startup projects 并运行它们。或者,可以通过激活相应 Debug 或 Release 目录中的 LoginService.exe 和 GraphQlService.exe 文件来启动服务。在这种情况下,应手动启动浏览器,导航到 https://:5001/playground,前提是服务已在运行。
首先,您需要从
Postman
向 https://:5011/login 发送一个 POST 请求,提供用户的凭据username = "Super", password = "SuperPassword"
。请注意端口 5011:正如您所见,LoginService
在此端口上监听。图 11: 登录然后,在 Postman 中打开一个新的选项卡,向 https://:5001/gql 发送 POST 请求,打开 Authorization -> Bearer Token,将登录时收到的令牌复制到 Token 文本框中,然后单击 Send 按钮发送请求。您可以使用
OpenApi
进行身份验证。为此,在OpenApi
网页中,单击 Authorize 按钮(参见图 7),在 Value 文本框中输入单词 "Bearer
",后跟 JWT 令牌,然后单击 Authorize 按钮。 -
集成测试可以在 ServicesTest 项目的 .\Test 目录中找到。
使用 Playground 进行查询和突变
Playground
是一个 Web 应用程序,可以通过 GraphQL 库的中间件即开即用地激活(在本例中,使用了 NuGet 包 GraphQL.Server.Ui.Playground
)。它提供了一种方便直观的方式来定义、文档化和执行 GraphQL 查询和突变。Playground
提供智能感知、错误处理和单词提示。它还显示 GraphQL 架构以及给定任务的所有可用查询和突变。Playground
的屏幕截图如上图 4 所示。
这些是我们解决方案的查询和突变示例。您可以在 Playground
的 DOCS 窗格中看到它们的描述。
以下 Persons
查询返回所有人员。
query Persons {
personQuery {
persons {
id
givenName
surname
affiliations {
organization {
name
parent {
name
}
}
role {
name
}
}
relations {
p2 {
givenName
surname
}
kind
notes
}
}
}
}
PersonById
查询通过其唯一的 id
参数返回单个人员。在以下示例中,id
设置为 1
。
query PersonById {
personByIdQuery {
personById(id: 1) {
id
givenName
surname
relations {
p2 {
id
givenName
surname
}
kind
}
affiliations {
organization {
name
}
role {
name
}
}
}
}
}
PersonMutation
突变允许用户创建新人员或更新现有人员。
mutation PersonMutation {
personMutation {
createPersons(
personsInput: [
{
givenName: "Vasya"
surname: "Pupkin"
born: 1990
phone: "111-222-333"
email: "vpupkin@ua.com"
address: "21, Torn Street"
affiliations: [{ since: 2000, organizationId: -4, roleId: -1 }]
relations: [{ since: 2017, kind: "friend", notes: "*!", p2Id: -1 }]
}
{
givenName: "Antony"
surname: "Fields"
born: 1995
phone: "123-122-331"
email: "afields@ua.com"
address: "30, Torn Street"
affiliations: [{ since: 2015, organizationId: -3, roleId: -1 }]
relations: [
{ since: 2017, kind: "friend", notes: "*!", p2Id: -2 }
{ since: 2017, kind: "friend", notes: "*!", p2Id: 1 }
]
}
]
) {
status
message
}
}
}
测试
集成测试位于 ServicesTest
项目(.\Tests 目录)中。内存中服务用于集成测试。这种方法大大减少了开发集成测试的工作量。测试可以即开即用,因为它们会创建并初始填充数据库。
结论
这项工作讨论了使用 GraphQL 技术对事务性数据存储库进行 CRUD 操作,并展示了在 .NET 5 C# 中开发的相应服务。它还实现了 JWT 身份验证、OpenAPI、可配置日志和使用内存中服务的集成测试等有用功能。
历史
- 2021 年 2 月 28 日:初始版本