Logo
Section 1 / 6

开始使用 GraphQL 认证

学习如何在 GraphQL 中实现认证。

导入

开始使用 GraphQL 认证

学习如何在 GraphQL 中实现认证。

到目前为止,我们仅对认证的实现方式做出了一些设计选择。现在,让我们探索如何实现一个新的变更操作来认证用户,并使用服务器端的 Cookie 将认证令牌发送给发起请求的客户端。

我们将从后端实现认证。这个过程可能有些复杂,但我们会逐步进行:

  • 定义新的变更操作用于登录和登出。
  • 在数据库中存储用户的密码信息。我们存储的是密码哈希值而非明文密码,稍后会详细说明这一点。
  • 实现 login 变更操作以设置 Cookie,并更新我们的 GraphQL 服务器,使其能够从传入的请求中读取 JWT 令牌。
  • 限制所有其他变更操作仅对已认证用户开放。

这涉及的内容相当多,因此本节课我们将完成所有准备工作,其余步骤将在后续课程中继续实现。

新的模式定义

我们首先在应用中定义新的变更操作。添加一个用于登录应用的变更操作和另一个用于登出的变更操作。

1type Mutation { 2 login(userName: String!, password: String!): LogInResponse! 3 logOut: Boolean! 4} 5 6type LogInResponse { 7 expiresIn: Int! 8 user: User! 9}

login 变更操作接收用户名和密码,如果登录成功,它将设置一个包含 JWT 令牌的服务器端 Cookie,该令牌描述了已登录的用户。虽然我们不需要在 GraphQL 模式中声明 Cookie,但此变更操作将返回一个使用 LogInResponse 类型的 GraphQL 响应。

LogInResponse 类型包含已认证用户的信息以及 JWT 令牌的过期时间。这看起来像是重复信息,因为我们可以将相同的数据放入服务器端 Cookie 的 JWT 令牌中,但我们必须这样做,因为客户端应用无法读取 Cookie 的内容。

logOut 变更操作则简单得多,它只是指示浏览器删除由 login 方法返回的认证 Cookie。

密码处理

在实现变更操作之前,我们需要为用户设置密码。以明文形式存储密码是绝对不可接受的!假设有人入侵了我们的系统并获取了凭证数据库?在这种情况下,他们可以使用这些密码登录我们的网站,而且由于很多人在不同网站上重复使用密码,他们还可能尝试攻破用户的其他账户。

相反,我们应该存储用户密码的哈希值。生成密码哈希只需将密码传递给哈希函数,该函数会返回一个哈希值。

所有哈希函数都有两个重要特性:第一,相同的输入总是产生相同的输出;第二,输出看起来几乎随机。这种哈希计算对任何输入都很容易,但几乎不可能反推出生成特定哈希值的原始输入。

采用这种方法,当服务器收到用户的凭据时,它会对接收到的密码计算哈希值,并比较哈希值而不是直接比较密码本身。这样我们就能安全地验证用户是否提供了正确的密码,而无需存储原始密码。

当然,我们不会自行实现这一功能,而是依赖于一个广受认可且经过实战检验的 bcrypt 库。首先,我们需要安装这个库。

1npm install bcrypt

现在有了该库,一旦获得密码,就可以立即对其进行哈希处理。为此,我们使用 hashSync 函数。

1> bcrypt = require('bcrypt') 2> bcrypt.hashSync('password', 10) 3'$2b$10$gAVM2Uce.oI/PSNprCZM7.JrTtTW79mvuz0YTBUM8ySe0PD5x0Sjq'

hashSync 函数的第一个参数是要哈希的密码,第二个参数定义了哈希生成所需的资源开销程度。数值越高,生成哈希所需的时间越长,也越难被暴力破解(即攻击者尝试所有可能的密码)。

hashSync 中的 Sync 表示应用程序执行会被阻塞,直到生成出最终的哈希值。

我们需要在 MongoDB 模式中为 users 集合添加一个新字段,用于存储生成的密码哈希。

1// server/src/models/User.js 2 3const UserSchema = new Schema({ 4 userName: String, 5 fullName: String, 6 passwordHash: String, // 用于存储密码哈希的新字段 7}) 8 9module.exports = mongoose.model('User', UserSchema)

现在,我们可以为用户添加密码。为此,需要更新填充 users 集合的迁移脚本,并填充新字段。我们将保持简单,让每个用户的密码与其用户名相同。

1const { getObjectId } = require('mongo-seeding') 2const bcrypt = require('bcrypt') 3 4module.exports = [ 5 { 6 _id: getObjectId('ellen'), 7 userName: 'ellen', 8 fullName: 'Ellen James', 9 passwordHash: bcrypt.hashSync('ellen', 10), 10 }, 11 { 12 _id: getObjectId('peter'), 13 userName: 'peter', 14 fullName: 'Peter Miles', 15 passwordHash: bcrypt.hashSync('peter', 10), 16 }, 17]

认证流程概览

在编写 GraphQL 代码之前,我们先简要了解一下整个流程的工作原理。稍后我们会更详细地讨论。下图展示了应用主要组件之间的关系。

...
认证流程概览

Apollo Server 会在请求中存在 Cookie 时读取它,提取出 userId,并将该值传递给处理请求的解析器。为了传递如 userId 这样的额外数据,Apollo Server 会创建一个上下文对象,该对象对所有解析器都可用。上下文还包含用于与 HTTP 请求和响应交互的其他对象。

随后,解析器将使用上下文来处理请求。login 解析器会检查凭据并设置响应中的 Cookie,而 createProduct 解析器则会在创建产品前检查用户是否已认证。

使用 apollo-server-express

为了实现认证,我们的应用需要能够与 Cookie 交互。当前的配置无法做到这一点。幸运的是,Apollo Server 支持与 Express 框架 集成,该框架可以运行 Apollo Server、处理传入请求,并允许我们读取和设置 Cookie。

为此,我们需要添加一些新依赖项,例如 express 和 apollo-server-express,后者将 Apollo Server 与 Express 集成。

1npm install apollo-server-express apollo-server-core express graphql

现在,我们需要更改 Apollo Server 的设置方式,使用 apollo-server-express。这是之前的写法:

1require('./mongo.js') 2const { ApolloServer } = require('apollo-server') 3const { readSchema } = require('./schema.js') 4const { resolvers } = require('./resolvers.js') 5const logger = require('./logger.js') 6 7const typeDefs = readSchema() 8 9const server = new ApolloServer({ 10 typeDefs, 11 resolvers, 12}) 13 14server.listen().then(() => { 15 logger.info('Listening on port 4000') 16})

新的实现方式与之前非常相似,但在代码组织上有一些差异。

1require('./mongo.js') 2 3const { readSchema } = require('./schema.js') 4const { resolvers } = require('./resolvers.js') 5const logger = require('./logger.js') 6// 新增导入 7const { ApolloServer } = require('apollo-server-express') 8const express = require('express') 9 10async function startApp() { 11 const typeDefs = readSchema() 12 const app = express() 13 const server = new ApolloServer({ 14 typeDefs, 15 resolvers, 16 }) 17 18 await server.start() 19 server.applyMiddleware({ app, cors: false }) 20 app.listen({ port: 4000 }) 21} 22 23startApp().then(() => { 24 logger.info('Listening on port 4000') 25})

我们使用 express() 函数创建一个 Express 服务器实例。现在,Express 负责处理传入的 HTTP 请求。然后我们需要通过 applyMiddleware 函数使用 Express 来运行 Apollo Server。最后,app.listen 函数在端口 4000 上启动 Express 服务器,从而运行 Apollo Server。

所有这些代码都被封装在 startApp 函数中,因此我们需要执行该函数才能启动服务器。由于这是一个异步函数,它返回一个 Promise,所以我们需要用 then 函数等待其执行完成。

现在,我们将设置变得更复杂一些。首先,我们需要添加额外的 Express 中间件来实现认证。中间件是一种插件,可拦截请求和响应,为 Express 添加额外功能。

其次,我们将 Express 的请求和响应对象传递给解析器。为此,我们使用 Apollo 的一个新功能——context,它允许将某些全局对象传递给所有解析器。

我们还需要向应用添加两个中间件:

  • cookieParser:使我们的应用能够读取传入的 Cookie。
  • cors:允许 Apollo Studio 调用我们的应用。出于演示目的,我们需要此中间件来绕过浏览器的 CORS 限制,但在生产环境中,若客户端和服务器使用同一域名,则可以省略。

这是配置 Express 中间件的方式:

1const cors = require('cors') 2const cookieParser = require('cookie-parser') 3... 4const app = express() 5// 添加 CORS 中间件 6app.use(cors({ 7 // 允许来自 Apollo Studio 的请求 8 origin: ['https://studio.apollographql.com'], 9 // 允许 Apollo Studio 发送 Cookie 10 credentials: true, 11})) 12// 应用 Cookie 解析中间件 13app.use(cookieParser()) 14 15const server = new ApolloServer({ 16 typeDefs, 17 resolvers, 18 context: ({req, res}) => { 19 // 将 Express 的请求和响应对象传递给解析器 20 return { 21 req, 22 res 23 } 24 } 25}) 26 27await server.start() 28server.applyMiddleware({ app, cors: false })

在这段代码中,我们添加了 cors 和 cookieParser 中间件,然后在最后一行应用它们。CORS 中间件指定允许来自 Apollo Studio 的请求,并允许从 Apollo Studio 发送凭证。

总结

我们开始在 GraphQL 服务器中实现认证。首先,我们介绍了为支持应用授权而需实现的新 GraphQL 变更操作。接着,我们讨论了如何在应用中存储密码,以及为何需要使用哈希函数。课程末尾,我们了解了如何更新应用以支持 Cookie 机制。

开发前端应用

身份验证和授权