65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2021年2月28日

CPOL

10分钟阅读

viewsIcon

15444

downloadIcon

452

实现 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 操作
  • 现成的、方便的 PlaygroundGraphiQL Web UI 应用程序,用于 GraphQL 查询和突变,无需前端代码
  • JWT 身份验证
  • OpenAPI(又名 Swagger)与 GraphQL 结合使用
  • 灵活的可配置日志记录(目前仅配置为向控制台输出最少信息)
  • 使用内存中服务进行集成测试

使用了 NuGet 中提供的几个用于 GraphQL 开发的开源包。

服务如何工作?

服务的工作流程如下图所示

Flow

图 1. 服务的工作流程。

为了开始工作,用户向 LoginService (1) 提供凭据(用户名和密码)。后者生成 JWT 并将其返回给用户。然后用户将查询/更新发送到 GraphQlService (2) 并从服务接收响应。

这些服务有单独的数据库。LoginService 访问的用户数据库 UsersDb 包含一个 Users 表,该表包含每个用户的用户名、密码(实际应用中是加密的)和角色。GraphQlService 访问的 Person 数据库 PersonsDb 包含几个与人员、组织及其关系和隶属关系相关的表。

GraphQL 的使用和优化

GraphQL 定义了客户端和服务器之间关于数据检索(查询)和更新(突变)的契约。查询和突变都构成类似 JSON 的结构。检索到的数据以与请求非常相似的结构格式化并返回给客户端。由于 GraphQL 查询的层次结构,数据检索过程是一个对嵌套字段处理程序调用的序列。

GraphQL 暗示对每个数据字段使用解析器函数(resolvers)。通常,GraphQL 的实现(包括此处使用的实现)在形成 Web 响应层次结构期间确保调用适当的解析器。如果每个解析器都向数据库发出一个 SELECT 查询,那么这些查询的总数将等于层次结构中每个级别的返回行数。考虑一个获取所有人员的查询。在最顶层,获取所有 n 个人员。然后,在第二层,执行 nSELECT 查询以获取每个人员的隶属关系和关系。在后续的每个级别都会观察到类似的情况。显然,大量的返回行会导致大量的数据库查询,从而导致严重的性能损失。此情况下的数据库查询数量为:

database_queries = Σ entries(level - 1)
                                 levels

这个问题通常被称为 N + 1 查询问题

高效的 GraphQL 实现必须为此问题提供合理的解决方案。本工作中实现的解决方案可以表述如下。在“朴素”实现中,每个字段的处理程序都会调用数据库来检索数据。在优化解决方案中,每个级别上字段处理程序的第一次调用将从数据库检索该级别所有字段的数据,并将其存储在附加到 GraphQL 上下文对象的缓存中。GraphQL 上下文对象可供所有字段处理程序使用。同一级别字段处理程序的后续调用将从缓存中获取数据,而不是从数据库中获取。在优化情况下,数据库查询数量为:

database_queries = levels

您可以看到,数据库调用(SELECT)的数量对应于 GraphQL 查询的内部级别数,并且与每个级别上获取的记录数无关。

图 2 和图 3 所示说明了这种差异

Non-optimized data fetch

图 2. 未优化的数据获取。

Optimized data fetch

图 3. 优化的数据获取。

解析器的返回值将自动插入到 GraphQL 查询的响应对象中。我们只需要在当前级别的解析器中提供已从数据库中获取的数据的返回值。最简单的方法是将获取的数据放入内存中的缓存对象,并将其附加到可供所有解析器使用的上下文对象。缓存组织为一个字典,其键根据解析器命名。每个解析器从缓存中提取一个具有相应键的数据片段并返回。

这些返回值将构成在 GraphQL 查询处理结束时发送回客户端的响应对象。由于缓存对象是上下文对象的属性,因此它将与上下文对象一起在 GraphQL 查询处理结束时被销毁。因此,缓存对象是为每个客户端请求创建的,其生命周期仅限于该请求的处理。

基于内存缓存的数据获取优化可以提高性能。但其限制在于可用运行内存(RAM)的大小。如果缓存对象太大,无法容纳在单个进程内存中,则可以使用分布式缓存解决方案,例如 RedisMemcached 或类似的解决方案。在本文中,我们假设使用简单的内存缓存,这足以满足绝大多数实际情况。

组件和结构

组件 (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 应用程序

操作顺序

  1. 使用 VS2019 打开 GraphQL_DotNet.sln 解决方案,该解决方案支持 .NET 5,并构建解决方案。

  2. 使用 SQL Server。为简化起见,采用了 Code First 范例。在运行相应的服务或其集成测试时,会自动创建 UsersDbPersonsDb 数据库。如果需要,请调整 appsettings.json 服务配置文件中的连接字符串。启动时,数据库会从代码中填充一些初始记录。为了确保标识机制的正常运行,所有这些记录都被分配了负数 Id,除了 UsersDb.Users,因为该表在此工作中不会被程序化更改。

  3. GraphQlService 的配置文件 appsetting.json 包含 FeatureToggles 对象。
    "FeatureToggles": {
    	"IsAuthJwt": true,
    	"IsOpenApiSwagger": true,
    	"IsGraphIql": true,
    	"IsGraphQLPlayground": true,
    	"IsGraphQLSchema": true
    }

    默认情况下,所有选项都设置为 true。让我们先开始不进行身份验证,并将 "IsAuthJwt" 设置为 false

  4. 启动 GraphQlService。可以从 VS2019 以服务形式或在 IIS Express 下启动。带有 GraphQL Playground Web UI 应用程序的浏览器会自动启动。

    Playground 网页中,您可以查看 GraphQL 架构,并尝试不同的查询和突变。一些预定义的查询和突变可以从文件 queries-mutations-examples.txt 中复制。

    Playground Web application

    图 4. Playground Web 应用程序。

    您可以使用类似的 GraphiQL Web 应用程序代替 Playground:浏览到 https://:5001/graphiql

    GraphiQL Web application

    图 5. GraphiQL Web 应用程序。
  5. Playground 应用程序使用中间件直接获取响应,绕过 GqlController(它主要在开发过程中使用,但在本项目中,在所有版本中都可用)。它不调用 GqlController,而 GqlController 是生产环境中供客户端使用的。要使用 GqlController,您可以使用 Postman 应用程序。

    Postman 中,向 https://:5001/gql 发送一个 POST 请求,在 Body -> GraphQL 中,在 QUERY 文本框中输入您的实际 GraphQL 查询/突变。

    GraphQL query with authentication

    图 6. 使用 Postman 进行 GraphQL 查询。
  6. 您也可以使用 OpenApi(又名 Swagger):浏览到 https://:5001/swagger

    OpenAPi (Swagger)

    图 7. OpenAPI(Swagger)。

    在 Swagger 网页中,激活 POST /Gql

    然后在 Postman 中,点击右上角的 Code 链接。

    HTTP request from Postman

    图 8. Postman 的 HTTP 请求

    将查询复制到 Swagger 的 Request body 文本框并执行方法。

    POST /Gql request

    图 9. POST /Gql 请求。

    POST /Gql response

    图 10. POST /Gql 响应。
  7. 在所有情况下,您都可以使用不安全的 HTTP 调用 https://:5000,这允许用于说明和调试。

  8. 现在,让我们使用 JWT 身份验证。停止运行 GraphQlService(如果正在运行),在 GraphQlService 的配置文件 appsetting.jsonFeatureToggle 对象中,将 "IsAuthJwt" 设置为 true,在 VS2019 中,将 LoginServiceGraphQlService 定义为 Multiple startup projects 并运行它们。

    或者,可以通过激活相应 DebugRelease 目录中的 LoginService.exeGraphQlService.exe 文件来启动服务。在这种情况下,应手动启动浏览器,导航到 https://:5001/playground,前提是服务已在运行。

    首先,您需要从 Postmanhttps://: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 按钮。

  9. 集成测试可以在 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 日:初始版本
© . All rights reserved.