使用 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