发布于 2026-01-06 3 阅读
0

使用 JWT、Bcrypt 和 GraphQL Nexus 实现身份验证

使用 JWT、Bcrypt 和 GraphQL Nexus 实现身份验证

你已经完成了应用程序的框架代码,但还缺少一样东西——身份验证。这可以通过使用 JSON Web Tokens 和 Bcrypt 来实现。本教程的基本原理适用于大多数模式构建框架,但我们将使用 GraphQL Nexus。我们还使用 Prisma 作为 ORM,但任何其他 ORM 或数据库都可以。

本教程假设您已了解 GraphQL mutation、queries、resolvers 和 context - 如果您不了解 GraphQL,《  How to GraphQL》 是一个很好的入门资源。

最终版应用程序将允许用户通过存储和使用 JSON Web Token (JWT) 来创建账户并登录。JWT 是包含需要在各方之间传输的信息的字符串,是验证用户身份的绝佳方式,因为它可以安全地存储用户信息并提供数字签名。

我们的应用程序将允许用户使用这些 JWT 登录和注册。在后端,我们将创建有效负载,添加 JWT 密钥,并设置登录和注册变更以正确生成授权标头。在前端,我们将把授权令牌传递到标头中,并设置查询以获取当前登录用户。

后端

1. 安装我们的工具🛠

首先,我们需要安装 Bcrypt 和 JSON Web Tokens!

yarn add bcrypt jsonwebtoken

现在你已准备好开始✨

2. 打造我们的 JWT 秘密 🗝️

我们可以设置 JWT 密钥——在我们的config.ts文件中,添加了以下内容:

export default {  
  ...  
  jwt: {  
    JWT_SECRET: 'super-secret',  
  },  
}

3. 创建有效载荷🚚

为了正确地将令牌和用户信息返回给请求者,我们需要设置有效负载。

export const UserLoginPayload = objectType({  
  name: 'UserLoginPayload',  
  definition: t => {  
    t.field('user', {  
      type: 'User',  
    })  
    t.string('token')  
  },  
})

我们在这里创建了一个名为 的对象类型  userLoginPayload。我们将该类型定义为能够返回我们的 User 字段,以及用户注册或登录时生成的令牌。

4. 设置登录和注册变更 🚪🚶

为了设置用户注册和登录,我们创建了两个新的 mutation 字段:`userregister` 和 `  userLogin userpassword`  userRegister。我们可以将返回类型设置为  `true` UserLoginPayload 来返回 `userregister` 和 `userpassword`,参数分别是从前端表单收集的用户名和密码。以下是这些 mutation 在 GraphQL Nexus 中的实现示例:Usertoken

export const userLogin = mutationField('userLogin', {  
  type: UserLoginPayload,  
  args: {  
    username: stringArg({ required: true }),  
    password: stringArg({ required: true }),  
  },  
})

export const userRegister = mutationField('userRegister', {  
  type: UserLoginPayload,  
  args: {  
    username: stringArg({ required: true }),  
    password: stringArg({ required: true }),  
  },  
})

之后,将解析器添加到突变中。

export const userLogin = mutationField('userLogin', {  
  type: UserLoginPayload,  
  args: {  
    username: stringArg({ required: true }),  
    password: stringArg({ required: true }),  
  },  
  resolve: async (root, args, context, info) => {  
    try {  
      const { password, ...user } = await context.prisma.user({  
        where: {  
          userName: args.username,  
        },  
      })  
      var validpass = await bcrypt.compareSync(args.password, password)  
      if (validpass) {  
        const token = jwt.sign(user, config.jwt.JWT_SECRET)  
        return {  
          user: user,  
          token,  
        }  
      }  
      return null  
    } catch (e) {  
      console.log(e)  
    }  
  },  
})

我们已经添加了解析器。这可能有点复杂,所以让我们把它分成几个部分来讲解。

const { password, ...user } = await context.prisma.user({  
        where: {  
          userName: args.username,  
        },  
      })

在这里,我们尝试获取 User 数据。 await context.prisma.users({where: {userName: args.username} 它从数据库中获取 User 信息,并将信息存储在 中 password, ...user。我们已经将密码分离出来,这样它就不会包含在我们的用户变量或 JSON Web Token 数据中,如下一步所示。

var validpass = await bcrypt.compareSync(args.password, password)  
      if (validpass) {  
        const token = jwt.sign(user, config.jwt.JWT_SECRET)  
        return {  
          user: user,  
          token,  
        }  
      }  
      return null

我们使用 Bcrypt 来比较密码值是否相等。如果密码匹配,则会使用配置文件中的 JWT 密钥生成 JWT  user。(如果我们没有事先分离密码数据,它会和用户数据一起返回并存储在 JWT 中😱!)不过,现在我们终于可以返回有效载荷(数据 user 和 JWT)了!

注册流程大致相同。

export const userRegister = mutationField('userRegister', {  
  type: UserLoginPayload,  
  args: {  
    username: stringArg({ required: true }),  
    password: stringArg({ required: true }),  
  },  
  resolve: async (root, args, context) => {  
    try {  
      const existingUser = await context.prisma.user({  
        where: {  
          userName: args.username,  
        },  
      })  
      if (existingUser) {  
        throw new Error('ERROR: Username already used.')  
      }  
      var hash = bcrypt.hashSync(args.password, 10)

      const { password, ...register } = await context.prisma.createUser({  
        userName: args.username,  
        password: hash,  
      })  
      const token = jwt.sign(register, config.jwt.JWT_SECRET)  
      return {  
        user: register,  
        token: token,  
      }  
    } catch (e) {  
      console.log(e)  
      return null  
    }  
  },  
})

我们再来分析一下。

const existingUser = await context.prisma.user({  
        where: {  
          userName: args.username,  
        },  
      })  
      if (existingUser) {  
        throw new Error('ERROR: Username already used.')  
      }

之前,我们会查询用户名是否存在。现在的操作基本相同,只是如果返回了任何内容,我们会抛出一个错误,因为每个用户名都应该是唯一的。

var hash = bcrypt.hashSync(args.password, 10)

      const { password, ...register } = await context.prisma.createUser({  
        userName: args.username,  
        password: hash,  
      })

我们使用 bcrypt 对表单中传入的密码进行哈希处理,传入的参数是密码和我们想要生成的盐值长度。之后,该 createUser操作会创建一个新用户,用户名和新生成的哈希密码与我们提供的用户名相同。

const token = jwt.sign(register, config.jwt.JWT_SECRET)  
      return {  
        user: register,  
        token: token,  
      }

有效载荷的生成和返回方式与用户登录方式相同。

5. 将用户添加到上下文中🧮

用户现在可以登录和注册了!现在我们可以创建一个查询和查看字段,将该信息返回给前端。

首先,让我们将当前用户添加到上下文中。

export interface Context {  
  prisma: Prisma  
  currentUser: User  
}

export default async ({ req }) => {  
  const currentUser = await getUser(  
    req.get('Authorization'),  
    config.jwt,  
    prisma,  
  )  
  return {  
    prisma,  
    currentUser  
  }  
}

 在这里,我们将添加要从我们的 中导出 currentUser 的类型 变量 。我们可以使用一个  函数(我们将在下一步中介绍如何创建此函数 - 简而言之,它返回我们的  类型)通过传入我们的令牌 (从我们的标头中获取我们的令牌)、我们的 JWT 密钥和 Prisma 客户端来返回我们的用户信息。UserContextgetUserUserreq.get('Authorization')

6. 创建 getUser 函数👶

因为我们需要在应用程序中查询用户信息,所以我们需要从请求头中获取用户的令牌。

export default async (authorization, secrets, prisma: Prisma) => {  
  const bearerLength = 'Bearer '.length  
  if (authorization && authorization.length > bearerLength) {  
    const token = authorization.slice(bearerLength)  
    const { ok, result } = await new Promise(resolve =>  
      jwt.verify(token, secrets.JWT_SECRET, (err, result) => {  
        if (err) {  
          resolve({  
            ok: false,  
            result: err,  
          })  
        } else {  
          resolve({  
            ok: true,  
            result,  
          })  
        }  
      }),  
    )  
    if (ok) {  
      const user = await prisma.user({  
        id: result.id,  
      })  
      return user  
    } else {  
      console.error(result)  
      return null  
    }  
  }  
  return null  
}

让我们一步一步来。

const bearerLength = 'Bearer '.length  
  if (authorization && authorization.length > bearerLength) {  
    const token = authorization.slice(bearerLength)  
    ...  
  }  
  return null  
}

这里我们进行一些基本的错误检查,看看标记是否比我们的 Bearer 字符串长——如果是,我们可以通过截断字符串来提取标记 Bearer

const { ok, result } = await new Promise(resolve =>  
      jwt.verify(token, secrets.JWT_SECRET, (err, result) => {  
        if (err) {  
          resolve({  
            ok: false,  
            result: err,  
          })  
        } else {  
          resolve({  
            ok: true,  
            result,  
          })  
        }  
      })  
    )

现在,我们正在使用我们的密钥验证令牌,并根据传入的令牌是否有效以及 result来自 JWT(这是我们的 user 类型)的信息来解决我们的承诺。

if (ok) {  
      const user = await prisma.user({  
        id: result.id,  
      })  
      return user  
    } else {  
      console.error(result)  
      return null  
    }  
  }

最后,如果令牌有效,我们将查询从令牌中获得的 ID 对应的用户并返回该用户!

7. 创建用户查询和查看器字段🔬

我们可以创建一个查看器字段和用户查询,以便我们能够查询应用程序中当前登录用户的信息。

t.string('getCurrentUser', {  
  resolve: async (root, args, context, info) => {  
    return context.prisma.user  
  },  
})

我们可以创建一个新的查询, getCurrentUser这将返回我们在函数中得到的值 Context ,这样我们就可以轻松地查询当前登录的任何用户!

最后,我们应该 viewer 在查询中添加一个字段。

t.field('viewer', {  
      type: 'User',  
      nullable: true,  
      resolve: (root, args, context) => {  
        return context.currentUser  
      },  
    })

这只是返回 currentUser 我们添加到上下文中的值。

前端

1. 登录和注册💎

现在后端已经完成,我们可以使用后端创建的解析器来实现一个简单的前端解决方案。

const SIGNUP_MUTATION = gql`  
  mutation UserRegister($username: String!, $password: String!) {  
    userRegister(username: $username, password: $password) {  
      user {  
        id  
        userName  
      }  
      token  
    }  
  }  
`;

这是一个简单的注册流程,当表单提交时会创建一个新用户。我们使用了 userRegister 后端创建的函数,只需传入用户名和密码,并返回所需的任何信息即可。

<Mutation  
    mutation={SIGNUP_MUTATION}  
    onCompleted={data => _confirm(data)}  
  >  
...  
</Mutation>

接下来,我们可以将注册变更添加到 Mutation 由提供的组件 中react-apollo。变更完成后,我们调用该函数 _confirm

_confirm = async data => {  
  const { token } = data.userLogin;  
  this._saveUserData(token);  
};

_saveUserData = async token => {  
  try {  
    await AsyncStorage.setItem(AUTH_TOKEN, token);  
  } catch (e) {  
    console.log("ERROR: ", e);  
  }  
};

我们的 _confirm 函数会获取 data mutation 返回的值,并从中提取 token,然后将其传递给 _saveUserData。该函数会将 token 存储 token 在 中 AsyncStorage (如果您不使用 Native 进行开发,则 token 将存储在 中 LocalStorage)。

警告:顺便提一下,在生产环境中使用 localStorage 存储 JWT 并不是最佳实践——您可以在这里阅读更多相关信息。

登录过程非常相似,我们只需将我们的 替换 SIGNUP_MUTATION 为我们的 即可 LOGIN_MUTATION

2. 将令牌插入到请求头中💯

const authLink = setContext(async (_, { headers }) => {  
  const token = await AsyncStorage.getItem(AUTH_TOKEN);  
  return {  
    headers: {  
      ...headers,  
      authorization: token ? `Bearer ${token}` : ""  
    }  
  };  
});

我们使用 apollo-link-context`s` setContext 函数来设置应用程序的请求头。我们从 `s` 获取授权令牌 AsyncStorage,然后将其存储在请求头中。

3. 查询用户信息🙆

由于我们付出了辛勤的努力,我们可以在应用程序中的任何位置查询用户信息——没错,就是这么简单!

const GET_USER = gql`  
  query getUser {  
    viewer {  
      id  
    }  
  }  
`;

结论

至此,您的身份验证已设置完毕!我们已创建解析器来返回所需的有效负载,并且可以在应用程序的任何位置查询当前登录用户。本教程的灵感来源于 Spencer Carli 的优秀教程《 使用 React Native 和 Apollo 进行 GraphQL 身份验证》  ——如果您想更深入地了解本教程中讲解的内容,不妨看看这篇教程。如果您有任何问题或建议,请随时留言、在 Twitter上联系我们,或访问我们的网站。谢谢!

文章来源:https://dev.to/novvum/implementing-authentication-using-jwt-bcrypt-and-graphql-nexus-29n6