grahql极简入门教程(基于react+graphql-yoga+urql)⑥

180 阅读6分钟

本文是graphql极简入门教程的第六篇,本篇内容主要讲述后端编写用户登录及注册功能

graphql极简入门教程目录:

👉🏻点击进入本教程github仓库

接入用户系统

数据库数据模型

在数据库中,需要添加用户(User)模型,该模型中将会包含以下的字段:

  • id:用户id
  • name:用户名称
  • email:用户email
  • password:用户密码(需要通过哈希处理后存储,后面会详细讲述)
  • 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
    }
  }
}

image-20230208234403467

后端成功创建了一个用户,并返回了tokenid字段。

接下来根据注册的内容,输入下面内容进行登录:

mutation {
  login(email: "orange@juejin.cn", password: "graphql") {
    token
    user {
      id
    }
  }
}

image-20230208234611286

恭喜你登录成功!你已经完成了后端的注册及登录功能

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情