koa项目实战 == 实现注册登录鉴权

news2024/11/25 16:56:29

一. 项目的初始化

1 npm 初始化

npm init -y

生成package.json文件:

  • 记录项目的依赖

2 git 初始化

git init

生成’.git’隐藏文件夹, git 的本地仓库

3 创建 ReadMe 文件

二. 搭建项目

1 安装 Koa 框架

npm install koa

2 编写最基本的 app

创建src/main.js

const Koa = require("koa");

const app = new Koa();

app.use((ctx, next) => {
  ctx.body = "hello world";
});

app.listen(3000, () => {
  console.log("server is running on http://localhost:3000");
});

3 测试

在终端, 使用node src/main.js

image-20210521142016066

三. 项目的基本优化

1 自动重启服务

安装 nodemon 工具

npm i nodemon -D

编写package.json脚本

"scripts": {
  "dev": "nodemon ./src/main.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},

执行npm run dev启动服务

image-20210521142807478

2 读取配置文件

安装dotenv, 读取根目录中的.env文件, 将配置写到process.env

npm i dotenv

创建.env文件

APP_PORT=8000

创建src/config/config.default.js

const dotenv = require("dotenv");

dotenv.config();

// console.log(process.env.APP_PORT)

module.exports = process.env;

改写main.js

const Koa = require("koa");

const { APP_PORT } = require("./config/config.default");

const app = new Koa();

app.use((ctx, next) => {
  ctx.body = "hello api";
});

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`);
});

四. 添加路由

路由: 根据不同的 URL, 调用对应处理函数

1 安装 koa-router

npm i koa-router

步骤:

  1. 导入包
  2. 实例化对象
  3. 编写路由
  4. 注册中间件

2 编写路由

创建src/router目录, 编写user.route.js

const Router = require("koa-router");

const router = new Router({ prefix: "/users" });

// GET /users/
router.get("/", (ctx, next) => {
  ctx.body = "hello users";
});

module.exports = router;

3 改写 main.js

const Koa = require("koa");

const { APP_PORT } = require("./config/config.default");

const userRouter = require("./router/user.route");

const app = new Koa();

app.use(userRouter.routes());

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`);
});

五. 目录结构优化

1 将 http 服务和 app 业务拆分

创建src/app/index.js

const Koa = require("koa");

const userRouter = require("../router/user.route");

const app = new Koa();

app.use(userRouter.routes());

module.exports = app;

改写main.js

const { APP_PORT } = require("./config/config.default");

const app = require("./app");

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`);
});

2 将路由和控制器拆分

路由: 解析 URL, 分布给控制器对应的方法

控制器: 处理不同的业务

改写user.route.js

const Router = require("koa-router");

const { register, login } = require("../controller/user.controller");

const router = new Router({ prefix: "/users" });

// 注册接口
router.post("/register", register);

// 登录接口
router.post("/login", login);

module.exports = router;

创建controller/user.controller.js

class UserController {
  async register(ctx, next) {
    ctx.body = "用户注册成功";
  }

  async login(ctx, next) {
    ctx.body = "登录成功";
  }
}

module.exports = new UserController();

六. 解析 body

1 安装 koa-body

npm i koa-body

2 注册中间件

改写app/index.js

image-20210521165536780

3 解析请求数据

改写user.controller.js文件

const { createUser } = require("../service/user.service");

class UserController {
  async register(ctx, next) {
    // 1. 获取数据
    // console.log(ctx.request.body)
    const { user_name, password } = ctx.request.body;
    // 2. 操作数据库
    const res = await createUser(user_name, password);
    // console.log(res)
    // 3. 返回结果
    ctx.body = ctx.request.body;
  }

  async login(ctx, next) {
    ctx.body = "登录成功";
  }
}

module.exports = new UserController();

4 拆分 service 层

service 层主要是做数据库处理

创建src/service/user.service.js

class UserService {
  async createUser(user_name, password) {
    // todo: 写入数据库
    return "写入数据库成功";
  }
}

module.exports = new UserService();

七. 集成 sequlize

sequelize ORM 数据库工具

ORM: 对象关系映射

  • 数据表映射(对应)一个类
  • 数据表中的数据行(记录)对应一个对象
  • 数据表字段对应对象的属性
  • 数据表的操作对应对象的方法

1 安装 sequelize

npm i mysql2 sequelize

2 连接数据库

src/db/seq.js

const { Sequelize } = require("sequelize");

const {
  MYSQL_HOST,
  MYSQL_PORT,
  MYSQL_USER,
  MYSQL_PWD,
  MYSQL_DB,
} = require("../config/config.default");

const seq = new Sequelize(MYSQL_DB, MYSQL_USER, MYSQL_PWD, {
  host: MYSQL_HOST,
  dialect: "mysql",
});

seq
  .authenticate()
  .then(() => {
    console.log("数据库连接成功");
  })
  .catch((err) => {
    console.log("数据库连接失败", err);
  });

module.exports = seq;

3 编写配置文件

APP_PORT = 8000

MYSQL_HOST = localhost
MYSQL_PORT = 3306
MYSQL_USER = root
MYSQL_PWD = 123456
MYSQL_DB = zdsc

八. 创建 User 模型

1 拆分 Model 层

sequelize 主要通过 Model 对应数据表

创建src/model/user.model.js

const { DataTypes } = require("sequelize");

const seq = require("../db/seq");

// 创建模型(Model zd_user -> 表 zd_users)
const User = seq.define("zd_user", {
  // id 会被sequelize自动创建, 管理
  user_name: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    comment: "用户名, 唯一",
  },
  password: {
    type: DataTypes.CHAR(64),
    allowNull: false,
    comment: "密码",
  },
  is_admin: {
    type: DataTypes.BOOLEAN,
    allowNull: false,
    defaultValue: 0,
    comment: "是否为管理员, 0: 不是管理员(默认); 1: 是管理员",
  },
});

// 强制同步数据库(创建数据表)
// User.sync({ force: true })

module.exports = User;

九. 添加用户入库

所有数据库的操作都在 Service 层完成, Service 调用 Model 完成数据库操作

改写src/service/user.service.js

const User = require("../model/use.model");

class UserService {
  async createUser(user_name, password) {
    // 插入数据
    // User.create({
    //   // 表的字段
    //   user_name: user_name,
    //   password: password
    // })

    // await表达式: promise对象的值
    const res = await User.create({ user_name, password });
    // console.log(res)

    return res.dataValues;
  }
}

module.exports = new UserService();

同时, 改写user.controller.js

const { createUser } = require("../service/user.service");

class UserController {
  async register(ctx, next) {
    // 1. 获取数据
    // console.log(ctx.request.body)
    const { user_name, password } = ctx.request.body;
    // 2. 操作数据库
    const res = await createUser(user_name, password);
    // console.log(res)
    // 3. 返回结果
    ctx.body = {
      code: 0,
      message: "用户注册成功",
      result: {
        id: res.id,
        user_name: res.user_name,
      },
    };
  }

  async login(ctx, next) {
    ctx.body = "登录成功";
  }
}

module.exports = new UserController();

十. 错误处理

在控制器中, 对不同的错误进行处理, 返回不同的提示错误提示, 提高代码质量

const { createUser, getUerInfo } = require("../service/user.service");

class UserController {
  async register(ctx, next) {
    // 1. 获取数据
    // console.log(ctx.request.body)
    const { user_name, password } = ctx.request.body;

    // 合法性
    if (!user_name || !password) {
      console.error("用户名或密码为空", ctx.request.body);
      ctx.status = 400;
      ctx.body = {
        code: "10001",
        message: "用户名或密码为空",
        result: "",
      };
      return;
    }
    // 合理性
    if (getUerInfo({ user_name })) {
      ctx.status = 409;
      ctx.body = {
        code: "10002",
        message: "用户已经存在",
        result: "",
      };
      return;
    }
    // 2. 操作数据库
    const res = await createUser(user_name, password);
    // console.log(res)
    // 3. 返回结果
    ctx.body = {
      code: 0,
      message: "用户注册成功",
      result: {
        id: res.id,
        user_name: res.user_name,
      },
    };
  }

  async login(ctx, next) {
    ctx.body = "登录成功";
  }
}

module.exports = new UserController();

在 service 中封装函数

const User = require("../model/use.model");

class UserService {
  async createUser(user_name, password) {
    // 插入数据
    // await表达式: promise对象的值
    const res = await User.create({ user_name, password });
    // console.log(res)

    return res.dataValues;
  }

  async getUerInfo({ id, user_name, password, is_admin }) {
    const whereOpt = {};

    id && Object.assign(whereOpt, { id });
    user_name && Object.assign(whereOpt, { user_name });
    password && Object.assign(whereOpt, { password });
    is_admin && Object.assign(whereOpt, { is_admin });

    const res = await User.findOne({
      attributes: ["id", "user_name", "password", "is_admin"],
      where: whereOpt,
    });

    return res ? res.dataValues : null;
  }
}

module.exports = new UserService();

十一. 拆分中间件

为了使代码的逻辑更加清晰, 我们可以拆分一个中间件层, 封装多个中间件函数

image-20210524154353520

1 拆分中间件

添加src/middleware/user.middleware.js

const { getUerInfo } = require("../service/user.service");
const { userFormateError, userAlreadyExited } = require("../constant/err.type");

const userValidator = async (ctx, next) => {
  const { user_name, password } = ctx.request.body;
  // 合法性
  if (!user_name || !password) {
    console.error("用户名或密码为空", ctx.request.body);
    ctx.app.emit("error", userFormateError, ctx);
    return;
  }

  await next();
};

const verifyUser = async (ctx, next) => {
  const { user_name } = ctx.request.body;

  if (getUerInfo({ user_name })) {
    ctx.app.emit("error", userAlreadyExited, ctx);
    return;
  }

  await next();
};

module.exports = {
  userValidator,
  verifyUser,
};

2 统一错误处理

  • 在出错的地方使用ctx.app.emit提交错误
  • 在 app 中通过app.on监听

编写统一的错误定义文件

module.exports = {
  userFormateError: {
    code: "10001",
    message: "用户名或密码为空",
    result: "",
  },
  userAlreadyExited: {
    code: "10002",
    message: "用户已经存在",
    result: "",
  },
};

3 错误处理函数

module.exports = (err, ctx) => {
  let status = 500;
  switch (err.code) {
    case "10001":
      status = 400;
      break;
    case "10002":
      status = 409;
      break;
    default:
      status = 500;
  }
  ctx.status = status;
  ctx.body = err;
};

改写app/index.js

const errHandler = require("./errHandler");
// 统一的错误处理
app.on("error", errHandler);

十二. 加密

在将密码保存到数据库之前, 要对密码进行加密处理

123123abc (加盐) 加盐加密

1 安装 bcryptjs

npm i bcryptjs

2 编写加密中间件

const crpytPassword = async (ctx, next) => {
  const { password } = ctx.request.body;

  const salt = bcrypt.genSaltSync(10);
  // hash保存的是 密文
  const hash = bcrypt.hashSync(password, salt);

  ctx.request.body.password = hash;

  await next();
};

3 在 router 中使用

改写user.router.js

const Router = require("koa-router");

const {
  userValidator,
  verifyUser,
  crpytPassword,
} = require("../middleware/user.middleware");
const { register, login } = require("../controller/user.controller");

const router = new Router({ prefix: "/users" });

// 注册接口
router.post("/register", userValidator, verifyUser, crpytPassword, register);

// 登录接口
router.post("/login", login);

module.exports = router;

十三. 登录验证

流程:

  • 验证格式
  • 验证用户是否存在
  • 验证密码是否匹配

改写src/middleware/user.middleware.js

const bcrypt = require("bcryptjs");

const { getUerInfo } = require("../service/user.service");
const {
  userFormateError,
  userAlreadyExited,
  userRegisterError,
  userDoesNotExist,
  userLoginError,
  invalidPassword,
} = require("../constant/err.type");

const userValidator = async (ctx, next) => {
  const { user_name, password } = ctx.request.body;
  // 合法性
  if (!user_name || !password) {
    console.error("用户名或密码为空", ctx.request.body);
    ctx.app.emit("error", userFormateError, ctx);
    return;
  }

  await next();
};

const verifyUser = async (ctx, next) => {
  const { user_name } = ctx.request.body;

  // if (await getUerInfo({ user_name })) {
  //   ctx.app.emit('error', userAlreadyExited, ctx)
  //   return
  // }
  try {
    const res = await getUerInfo({ user_name });

    if (res) {
      console.error("用户名已经存在", { user_name });
      ctx.app.emit("error", userAlreadyExited, ctx);
      return;
    }
  } catch (err) {
    console.error("获取用户信息错误", err);
    ctx.app.emit("error", userRegisterError, ctx);
    return;
  }

  await next();
};

const crpytPassword = async (ctx, next) => {
  const { password } = ctx.request.body;

  const salt = bcrypt.genSaltSync(10);
  // hash保存的是 密文
  const hash = bcrypt.hashSync(password, salt);

  ctx.request.body.password = hash;

  await next();
};

const verifyLogin = async (ctx, next) => {
  // 1. 判断用户是否存在(不存在:报错)
  const { user_name, password } = ctx.request.body;

  try {
    const res = await getUerInfo({ user_name });

    if (!res) {
      console.error("用户名不存在", { user_name });
      ctx.app.emit("error", userDoesNotExist, ctx);
      return;
    }

    // 2. 密码是否匹配(不匹配: 报错)
    if (!bcrypt.compareSync(password, res.password)) {
      ctx.app.emit("error", invalidPassword, ctx);
      return;
    }
  } catch (err) {
    console.error(err);
    return ctx.app.emit("error", userLoginError, ctx);
  }

  await next();
};

module.exports = {
  userValidator,
  verifyUser,
  crpytPassword,
  verifyLogin,
};

定义错误类型

module.exports = {
  userFormateError: {
    code: "10001",
    message: "用户名或密码为空",
    result: "",
  },
  userAlreadyExited: {
    code: "10002",
    message: "用户已经存在",
    result: "",
  },
  userRegisterError: {
    code: "10003",
    message: "用户注册错误",
    result: "",
  },
  userDoesNotExist: {
    code: "10004",
    message: "用户不存在",
    result: "",
  },
  userLoginError: {
    code: "10005",
    message: "用户登录失败",
    result: "",
  },
  invalidPassword: {
    code: "10006",
    message: "密码不匹配",
    result: "",
  },
};

改写路由

// 登录接口
router.post("/login", userValidator, verifyLogin, login);

十四. 用户的认证

登录成功后, 给用户颁发一个令牌 token, 用户在以后的每一次请求中携带这个令牌.

jwt: jsonwebtoken

  • header: 头部
  • payload: 载荷
  • signature: 签名

1 颁发 token

1) 安装 jsonwebtoken

npm i jsonwebtoken

2) 在控制器中改写 login 方法

async login(ctx, next) {
  const { user_name } = ctx.request.body

  // 1. 获取用户信息(在token的payload中, 记录id, user_name, is_admin)
  try {
    // 从返回结果对象中剔除password属性, 将剩下的属性放到res对象
    const { password, ...res } = await getUerInfo({ user_name })

    ctx.body = {
      code: 0,
      message: '用户登录成功',
      result: {
        token: jwt.sign(res, JWT_SECRET, { expiresIn: '1d' }),
      },
    }
  } catch (err) {
    console.error('用户登录失败', err)
  }
}

3) 定义私钥

.env定义

JWT_SECRET = xzd

2 用户认证

1) 创建 auth 中间件

const jwt = require("jsonwebtoken");

const { JWT_SECRET } = require("../config/config.default");

const { tokenExpiredError, invalidToken } = require("../constant/err.type");

const auth = async (ctx, next) => {
  const { authorization } = ctx.request.header;
  const token = authorization.replace("Bearer ", "");
  console.log(token);

  try {
    // user中包含了payload的信息(id, user_name, is_admin)
    const user = jwt.verify(token, JWT_SECRET);
    ctx.state.user = user;
  } catch (err) {
    switch (err.name) {
      case "TokenExpiredError":
        console.error("token已过期", err);
        return ctx.app.emit("error", tokenExpiredError, ctx);
      case "JsonWebTokenError":
        console.error("无效的token", err);
        return ctx.app.emit("error", invalidToken, ctx);
    }
  }

  await next();
};

module.exports = {
  auth,
};

2) 改写 router

// 修改密码接口
router.patch("/", auth, (ctx, next) => {
  console.log(ctx.state.user);
  ctx.body = "修改密码成功";
});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2234273.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA基础:单元测试;注解;枚举;网络编程 (学习笔记)

单元测试 操作步骤: a.导包import org.junit; b.三个注解 Test Before After c.点击Test 运行就可以了 用在不需要控制台输入的情境下:javaweb,框架项目,微服务项目 供开发人员自己做测试。 package com.page…

四个TikTok万能爆单选品法,第1个超过75%卖家会用!

做TK想爆单至关键的一步就是选品!7分靠选品、3分靠运营,一开始你要把品选错了,再怎么运营都是在浪费时间。接下来分享一些万能选品思路,不管做什么类目的商家都可以参考! 一、热卖品榜单选品 这是一种很常见&#xf…

Linux开发工具——make/Makefile

目录 一、什么是makefile? 二、为什么要有makefile? 三、makefile的使用 1.依赖关系与依赖方法 2.伪目标 3.定义变量 4.特殊符号 四、makefile的执行逻辑 一、什么是makefile? Makefile是一种自动化构建工具,make是一条指…

开发中使用UML的流程_01概述

目录 CIM-1:定义业务流程 CIM-2:分析业务流程 ​CIM-3:定义系统范围 ​PIM-1:分析系统流程 PIM-2:分析业务规则 PIM-3:定义静态结构 PIM-4:定义操作和方法 开发中使用UML的流程,主要分为7部分,具体如下: CIM-1:定义业务流程 定义及分析业务流程是为了尽快理…

前端开发模板Pear Admin Layui

目录 基本资料学习笔记04-Pear-Admin-Layui模板运行05-Pear-Admin-Layui-GIT方式代...06-Pear-Admin与Vue对比 & 07-Pear-Admin与Vue对比补充09-Pear-Admin-CRUD练习-数据库表创建12-Pear-Admin-CRUD练习-引入其它依赖 & 13-Pear-Admin-CRUD练习-三层架构以及常见配置 …

[MySQL#10] 索引底层(1) | Page | 页目录

目录 1. 初识索引 2. 认识磁盘 3. MySQL与磁盘交互基本单位 4. 索引的理解 1. 重谈Page 2. 为什么IO交互要用Page 3. 有主键的表插入数据时的排序 4. 单个Page与多个Page 4.1 单个Page 4.2 多个Page 目录 单Page目录 多Page目录 在看本文之前,可以回顾…

.net c# 使用 MailKit库接收139邮箱邮件

开发工具 vs2022,新建-控制台应用。项目完整代码下载: 要安装MailKit库 using MailKit; using MailKit.Net.Imap; using MimeKit; using System.Text; namespace MailKit_mail {internal class Program{static void Main(string[] args){//需要使用 Ma…

Centos开机自启动脚本示例

本文建议创建一个sh文件管理自启动的各项内容,再将sh文件设置开机启动 在/root/autoshell下创建一个autostart.sh,内容如下 #!/bin/bash # description:开机自启脚本# 启动mongodb sh /root/software/mongodb-linux-x86_64-rhel70-4.0.6/bin/mongod --c…

虚拟现实和增强现实技术,如何打造沉浸式体验?

内容概要 在这个科技飞速发展的时代,虚拟现实(VR)与增强现实(AR)技术的结合就像调皮的小精灵,一下子把我们的生活变得神奇又有趣。想象一下,你正在游戏中与精灵搏斗,突然间身边的客…

计算机网络 TCP/IP体系 数据链路层

一. 数据链路层的基本概念 数据链路层主要负责节点之间的通信,确保从物理层接收到的数据能够准确无误地传输到网络层。 数据链路层使用的信道主要有以下两种类型: 点对点信道: 这种信道使用一对一的点对点通信方式。广播信道: 这种信道使用一对多的广播通信方式,…

数据结构————链表

一、引言 1. 中间/头部的插入删除,时间复杂度为O(N) 2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。 3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200&#x…

【网络原理】深入理解关于HTTP协议和报文的格式以及重要的属性

前言 🌟🌟本期讲解关于HTTP协议的重要的机制~~~ 🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客 🔥 你的点赞就是小编不断更新的最大动力 🎆那么废话不…

【MyBatis源码】CacheKey缓存键的原理分析

文章目录 Mybatis缓存设计缓存KEY的设计CacheKey类主体CacheKey组成CacheKey如何保证缓存key的唯一性 Mybatis缓存设计 MyBatis 每秒过滤众多数据库查询操作,这对 MyBatis 缓存键的设计提出了很高的要求。MyBatis缓存键要满足以下几点。 无碰撞:必须保证…

面试题:JVM(二)

1. 面试题 简述 Java 类加载机制?(百度) JVM类加载机制 (滴滴) JVM中类加载机制,类加载过程,什么是双亲委派模型? (腾讯) JVM的类加载机制是什么? &#x…

数据库的使用02:SQLServer的连接字符串、备份、还原、SQL监视相关设置

目录 一、连接字符串 【本地连接字符串】 【远程连接字符串】 二、备份 三、还原 (1)还原数据库-bak、btn文件 (2)附加数据库mdf文件 四、SQL监视器的使用 一、连接字符串 【本地连接字符串】 server DESKTOP-FTH2P3S; Da…

【2024工业图像异常检测文献】UCAD: 使用对比学习提示的无监督连续异常检测方法

Unsupervised Continual Anomaly Detection with Contrastively-learned Prompt 1、Background 无监督异常检测(UAD)专注于在没有先验知识或标记实例的情况下识别数据中的不寻常模式或异常值,仅依赖于“正常”数据的内在分布(Cha…

【算法】Floyd多源最短路径算法

目录 一、概念 二、思路 三、代码 一、概念 在前面的学习中,我们已经接触了Dijkstra、Bellman-Ford等单源最短路径算法。但首先我们要知道何为单源最短路径,何为多源最短路径 单源最短路径:从图中选取一点,求这个点到图中其他…

[C++]——哈希(附源码)

目录 ​编辑 ​编辑 一、前言 二、正文 2.1 unorder系列关联式容器 2.1.1 unordered_map 2.1.1.1 unorderer_map的介绍 ①unordered_map的构造 ②unordered_map的容量 ③unordered_map的迭代器 ④unordered_map的元素访问 ⑤unordered_map的查询 ⑥unordered_map的修改操…

使用Ubuntu快速部署MinIO对象存储

想拥有自己的私有云存储,安全可靠又高效?MinIO是你的理想选择!这篇文章将手把手教你如何在Ubuntu 22.04服务器上部署MinIO,并使用Nginx反向代理和Let’s Encrypt证书进行安全加固。 即使你是新手,也能轻松完成&#xf…

Maven 下载配置 详解 我的学习笔记

Maven 下载配置 详解 我的学习笔记 一、Maven 简介二、maven安装配置三、maven基本使用四、idea配置mavenidea配置maven环境maven坐标idea创建maven项目配置Maven-Helper插件 五、依赖管理 一、Maven 简介 Apache Maven 是一个项目管理和构建工具,它基于项目对象模型…