本文是graphql极简入门教程的第六篇,本篇内容主要讲述后端编写用户登录及注册功能
graphql极简入门教程目录:
- 第一篇:基于react和graphql-yoga搭建前后端,并实现一个hello world
- 第二篇:基于prisma及sqlite,通过playground创建及查询数据
- 第三篇:在react中执行graphql的新增和查询操作
- 第四篇:react添加路由导航、前后端搜索功能
- 第五篇:添加分页及排序功能
- 第六篇:后端编写用户登录及注册功能
- 第七篇:前端对接用户系统
- 第八篇:前后端接入github的Oauth系统
- 第九篇:graphql实时订阅
接入用户系统
数据库数据模型
在数据库中,需要添加用户(User)模型,该模型中将会包含以下的字段:
id:用户idname:用户名称email:用户emailpassword:用户密码(需要通过哈希处理后存储,后面会详细讲述)links: 用户发布的链接
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model Link {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
description String
url String
+ postedBy User? @relation(fields: [postedById], references: [id])
+ postedById Int?
}
+model User {
+ id Int @id @default(autoincrement())
+ name String
+ email String @unique
+ password String
+ links Link[]
+}
请注意上面代码中如何设定关联关系的:
首先postedBy指向一个用户实例(Usee),并且是关系字段(@relation),在这里新建一个postById字段来存储用户的id信息,并且该字段等同于User模型里的id字段,同时User模型中的links字段也会记录与用户关联的链接内容。
切换到src/server目录下,执行下面的命令,迁移数据库:
cd src/server
npx prisma migrate dev --name "add-user-model"
并且通过下面的指令,生成新的prisma客户端:
npx prisma generate
定义后端数据模式
按照上面的数据表模型,不难写出下面的数据类型结构
type User {
id: ID!
name: String!
email: String!
links: [Link!]!
}
由于在登录时,后端会返回登录后用户的相关信息,因此也需要定义一个AuthPayload的类型
type AuthPayload {
token: String
user: User
}
里面的token将会使用jwt生成一个字符串的令牌(jwt后续会详细讲述)
接下来让我们在Mutation类型中添加注册及登录的定义:
type Mutation {
post(url: String!, description: String!): Link!
+ signup(email: String!, password: String!, name: String!): AuthPayload
+ login(email: String!, password: String!): AuthPayload
}
最后还需要在Link类型中添加postedBy字段,由于有可能没有用户信息,因此不添加!:
type Link {
id: ID!
url: String!
description: String!
createdAt: DateTime!
+ postedBy: User
}
最终src/server/schema.graphql文件经过上述的改动后,将会是下面的内容:
type Query {
feed(filter: String, skip: Int, take: Int, orderBy: LinkOrderByInput): Feed!
}
type AuthPayload {
token: String
user: User
}
type User {
id: ID!
name: String!
email: String!
links: [Link!]!
}
input LinkOrderByInput {
description: Sort
url: Sort
createdAt: Sort
}
enum Sort {
asc
desc
}
type Link {
id: ID!
url: String!
description: String!
createdAt: DateTime!
postedBy: User
}
type Feed {
links: [Link!]!
count: Int!
}
scalar DateTime
type Mutation {
post(url: String!, description: String!): Link!
signup(email: String!, password: String!, name: String!): AuthPayload
login(email: String!, password: String!): AuthPayload
}
实现后端数据逻辑
实现jwt工具类
由于社区当中对于jwt解读的文章很多,笔者就不在此造轮子了,附上阮一峰老师的入门链接,有兴趣的同学可以点击右侧查看👉🏻JSON Web Token 入门教程
在项目根目录下,先安装jsonwebtoken库:
npm install jsonwebtoken
在src/server/utils.js路径下,新增jwt工具类:
const jwt = require('jsonwebtoken');
const APP_SECRET = 'GraphQL-is-aw3some';
function getTokenPayload(token) {
return jwt.verify(token, APP_SECRET);
}
function getUserId(req, authToken) {
if (req) {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.replace('Bearer ', '');
if (!token) {
throw new Error('No token found');
}
const { userId } = getTokenPayload(token);
return userId;
}
} else if (authToken) {
const { userId } = getTokenPayload(authToken);
return userId;
}
throw new Error('Not authenticated');
}
module.exports = {
APP_SECRET,
getUserId
};
在工具类里从request中获取用户token,如果有token将会从jwt信息中解析出userId字段,如果没有则会抛出错误。
加密密码
本文为了简便,采用hash加密的方法加密用户输入的密码
请注意!!在生产环境中请使用更加安全和全面的加密方法,实例中的仅供教学简便使用
需要安装bcrypt.js来进行加密:
npm install bcryptjs
添加登录及注册逻辑
在src/server/resolvers/Mutation.js文件中添加下面的内容:
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { APP_SECRET } = require('../utils');
async function signup(parent, args, context, info) {
// ①
const password = await bcrypt.hash(args.password, 10)
// ②
const user = await context.prisma.user.create({ data: { ...args, password } })
// ③
const token = jwt.sign({ userId: user.id }, APP_SECRET)
// ④
return {
token,
user,
}
}
async function login(parent, args, context, info) {
// ⑤
const user = await context.prisma.user.findUnique({ where: { email: args.email } })
if (!user) {
throw new Error('No such user found')
}
// ⑥
const valid = await bcrypt.compare(args.password, user.password)
if (!valid) {
throw new Error('Invalid password')
}
const token = jwt.sign({ userId: user.id }, APP_SECRET)
// ⑦
return {
token,
user,
}
}
async function post(parent, args, context) {
const newLink = await context.prisma.link.create({
data: {
url: args.url,
description: args.description,
}
});
return newLink;
}
module.exports = {
signup,
login,
post,
}
下面将会一步步解析上面代码所做的事情:
首先是注册逻辑:
①:使用bcrypt.js库对用户输入的密码进行加密
②:通过prisma提供的createAPI,在User中创建用户信息
③:基于已设定APP_SECRET密钥生成jwt的token(请注意:这里APP_SECRET非常简单,仅做教学使用,生产环境请勿设置如此简单的内容)
④:按照AuthPayload定义的数据类型,返回用户注册成功后的信息
接着是登录逻辑:
⑤:通过prisma提供的findUniqueAPI,使用用户输入的email信息,查找到用户信息
⑥:比对密码,如果比对失败抛出错误,如果成功签发新的token
⑦:和注册一样,按照AuthPayload定义的数据类型,返回登录成功后的信息
将用户数据挂载在全局上下文中
为了在后端方便使用用户的UserId信息,需要将用户的UserId信息挂载在上下文中(context),修改src/server/index.js文件:
// ...
const fs = require("fs");
const path = require("path");
const Mutation = require("./resolvers/Mutation");
const Query = require("./resolvers/Query");
+ const { getUserId } = require('./utils');
const resolvers = {
Mutation,
Query
}
const schema = createSchema({
typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'),
resolvers: resolvers
})
const prisma = new PrismaClient();
// 基于graphql的scheme,创建一个graphql-yoga的实例
context: ({ req }) => {
return {
...req,
prisma,
+ userId: req ? getUserId(req) : null
}
}
// ...
创建链接时,添加用户信息
需要将userId存入链接数据中,因此需要更改src/server/resolvers/Mutation.js文件中的post方法:
async function post(parent, args, context) {
+ const { userId } = context;
const newLink = await context.prisma.link.create({
data: {
url: args.url,
description: args.description,
+ postedBy: { connect: { id: userId } },
}
});
return newLink;
}
创建关联字段的查询逻辑
由于graphql无法知道怎么从关联的定义中获取数据,因此还需要手动编写相关的逻辑代码查询,先编写Link类型中的postBy字段的实现逻辑。
在src/server/resolvers/目录下,创建Link.js文件:
function postedBy(parent, args, context) {
return context.prisma.link.findUnique({ where: { id: parent.id } }).postedBy()
}
module.exports = {
postedBy,
}
接下来编写User类型中的links字段的实现逻辑
在src/server/resolvers/目录下,创建Link.js文件:
function links(parent, args, context) {
return context.prisma.user.findUnique({ where: { id: parent.id } }).links()
}
module.exports = {
links,
}
在src/server/index.js文件中引入这两个文件:
//...
const fs = require("fs");
const path = require("path");
const Mutation = require("./resolvers/Mutation");
const Query = require("./resolvers/Query");
+ const User = require('./resolvers/User')
+ const Link = require('./resolvers/Link')
const { getUserId } = require('./utils');
const resolvers = {
Mutation,
Query,
+ User,
+ Link
}
const schema = createSchema({
typeDefs: fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8'),
resolvers: resolvers
})
// ...
验证注册及登录功能
打开http://localhost:4000/graphql页面,输入以下内容注册用户:
mutation {
signup(name: "orange", email: "orange@juejin.cn", password: "graphql") {
token
user {
id
}
}
}
后端成功创建了一个用户,并返回了token及id字段。
接下来根据注册的内容,输入下面内容进行登录:
mutation {
login(email: "orange@juejin.cn", password: "graphql") {
token
user {
id
}
}
}
恭喜你登录成功!你已经完成了后端的注册及登录功能
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情