当前位置: 首页 > article >正文

KAO2 入门到熟练 看这一篇文章就够了

在这里插入图片描述

KOA 介绍

官网地址: https://koa.bootcss.com/

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

文章目录

  • KOA 介绍
  • 一. 项目的初始化
    • 1 npm 初始化
    • 2 git 初始化
  • 二. 搭建项目
    • 1 安装 Koa 框架
    • 2 编写最基本的 app
    • 3 测试
  • 三. 项目的基本优化
    • 1 自动重启服务
    • 2 读取配置文件
    • 3 洋葱模型
  • 四. 添加路由
    • 1 安装 koa-router
    • 2 编写路由
    • 3 改写 main.js
    • 4.ctx对象
  • 五. 目录结构优化
    • 1 将 http 服务和 app 业务拆分
    • 2 将路由和控制器拆分
  • 六. 解析 body
    • 1 安装 koa-body
    • 2 注册中间件
    • 3 解析请求数据
    • 4 拆分 service 层
    • 5 洋葱模型
  • 七. 集成 sequlize
    • 1 安装 sequelize
    • 2 连接数据库
    • 3 编写配置文件
  • 八. 创建 User 模型
    • 1 拆分 Model 层
  • 九. 添加用户入库
  • 十. 错误处理
  • 十一. 拆分中间件
    • 1 拆分中间件
    • 2 统一错误处理
    • 3 错误处理函数
  • 十二. 加密
    • 1 安装 bcryptjs
    • 2 编写加密中间件
    • 3 在 router 中使用
  • 十三. 登录验证
  • 十四. 用户的认证
    • 1 颁发 token
      • 1) 安装 jsonwebtoken
      • 2) 在控制器中改写 login 方法
      • 3) 定义私钥
    • 2 用户认证
      • 1) 创建 auth 中间件
      • 2) 改写 router
  • 十五 接口练习
  • 十六 koa集成 模板引擎
    • EJS 的使用
    • art-template

一. 项目的初始化

1 npm 初始化

npm init -y

生成package.json文件:

  • 记录项目的依赖

2 git 初始化

git init

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

二. 搭建项目

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

三. 项目的基本优化

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启动服务

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}`)
})

3 洋葱模型

image.png

洋葱模型,就是将数据顺序传入到多个中间件中,让它们进行处理传递,并利用函数递归的特性,让我们可以在一个中间件内先执行前半部分逻辑,再执行之后的所有中间件的完整逻辑后,再掉转方向继续执行这个中间件的后半部分。

const Koa = require('koa');
const app = new Koa();
 
// 中间件1
app.use((ctx, next) => {
    console.log("<<<1");
    next();
    console.log("1>>>");
});
 
// 中间件 2 
app.use((ctx, next) => {
    console.log("<<<2");
    next();
    console.log("2>>>");
});
 
// 中间件 3 
app.use((ctx, next) => {
    console.log("<<<3");
    next();
    console.log("3>>>");
});
app.listen(8000, '0.0.0.0', () => {
    console.log(`Server is starting`);
});

在 koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。可以通过下图直观看出:

  • 输出的结果
<<<1
<<<2
<<<3
3>>>
2>>>
1>>>
const Koa = require('koa');
const app = new Koa();
 
// 中间件1
app.use(async(ctx, next) => {
    console.log("<<<1");
   await next();
    console.log("1>>>");
});
 
// 中间件 2 
app.use(async(ctx, next) => {
    console.log("<<<2");

   const axios= require('axios')    
   const res= await axios.get('http://www.baidu.com')
    next();
    console.log("2>>>");
});
 
// 中间件 3 
app.use((ctx, next) => {
    console.log("<<<3");
    next();
    console.log("3>>>");
});
app.listen(8000, '0.0.0.0', () => {
    console.log(`Server is starting`);
});

四. 添加路由

路由: 根据不同的 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}`)
})
  • mock 返回
router.get('/api/data', async (ctx) => {
  // 查询成功返回数据
  const data = {
    message: '查询成功',
    value: 'xx数据',
  };
  ctx.body = data;
});

router.post('/api/data', async (ctx) => {
  // 创建数据成功
  const data = {
    message: '创建成功',
    value: 'xx数据',
  };
  ctx.body = data;
});

router.put('/api/data', async (ctx) => {
  // 修改成功
  const data = {
    message: '修改成功',
    value: 'xx数据',
  };
  ctx.body = data;
});

router.delete('/api/data', async (ctx) => {
  // 删除成功
  const data = {
    message: '删除成功',
    value: 'xx数据',
  };
  ctx.body = data;
});
  • 链式调用
router
  .get('/api/data', async (ctx) => {
    ctx.body = {
      message: '查询成功',
      value: 'xx数据',
    };
  })
  .post('/api/data', async (ctx) => {
    ctx.body = {
      message: '创建成功',
      value: 'xx数据',
    };
  })
  .put('/api/data', async (ctx) => {
    ctx.body = {
      message: '修改成功',
      value: 'xx数据',
    };
  })
  .delete('/api/data', async (ctx) => {
    ctx.body = {
      message: '删除成功',
      value: 'xx数据',
    };
  });
// 定义处理数据的函数
const handleData = (ctx, method, message) => {
  const data = {
    message,
    value: 'xx数据',
  };
  ctx.body = data;
};

// 定义路由处理
router
  .get('/api/data', async (ctx) => {
    handleData(ctx, '查询成功', 'xx数据');
  })
  .post('/api/data', async (ctx) => {
    handleData(ctx, '创建成功', 'xx数据');
  })
  .put('/api/data', async (ctx) => {
    handleData(ctx, '修改成功', 'xx数据');
  })
  .delete('/api/data', async (ctx) => {
    handleData(ctx, '删除成功', 'xx数据');
  });

4.ctx对象

在 Koa 框架中,ctxcontext 的简写。context 是 Koa 内部提供的一个全局变量,它是一个带有请求和响应信息的上下文对象。ctx 对象封装了 context,提供了访问请求和响应的属性和方法的方便方式。

通过 ctx 对象,你可以访问 ctx.requestctx.responsectx.cookies 等属性来处理请求、响应和操作 cookie 数据。例如,ctx.request.query 用于获取查询参数,ctx.response.body 用于设置响应体内容,ctx.cookies.set() 用于设置 cookie 数据等。

因此,可以将 ctx 理解为上下文对象 context 的实例化对象,它提供了更简洁和易用的方式来访问和操作请求和响应的相关信息。通过使用 ctx,你可以在 Koa 应用程序的中间件中轻松地处理和操作请求和响应的数据。

  1. ctx.request:用于处理请求相关的属性和方法。
router.get('/api/data', async (ctx) => {
  // 获取请求参数
  const queryParam = ctx.request.query; // 获取查询参数
  const bodyParam = ctx.request.body; // 获取请求体参数

  // 设置响应状态码
  ctx.response.status = 200;

  // 设置响应类型
  ctx.response.type = 'application/json';

  // 设置响应头
  ctx.response.set('X-Custom-Header', 'Custom Value');

  // 发送响应
  ctx.response.body = { message: '请求成功' };
});
  1. ctx.response:用于处理响应相关的属性和方法。
router.post('/api/data', async (ctx, next) => {
  // 获取请求体参数
  const bodyParam = ctx.request.body;

  // 设置响应状态码
  ctx.response.status = 201;

  // 设置响应类型
  ctx.response.type = 'application/json';

  // 设置响应头
  ctx.response.set('X-Custom-Header', 'Custom Value');

  // 发送响应
  ctx.response.body = { message: '创建成功', data: bodyParam };

  await next();
});
  1. ctx.cookies:用于操作 cookie。
router.get('/api/cookie', async (ctx) => {
  // 设置 cookie
  ctx.cookies.set('name', 'value', {
    httpOnly: true,
    maxAge: 86400000, // 设置过期时间为一天
  });

  // 获取 cookie
  const name = ctx.cookies.get('name');

  ctx.body = { cookie: name };
});

通过这些属性,你可以方便地处理请求的参数、设置响应的状态码、类型、头部信息以及操作 cookie 数据。以上是一些简单的示例,实际使用中可能还会涉及更多的属性和方法,具体的使用取决于你的业务需求。

const Koa = require('koa');
const bodyParser = require('koa-bodyparser');

const app = new Koa();
app.use(bodyParser());

app.use(async (ctx) => {
  if (ctx.url === '/api/data' && ctx.method === 'POST') {
    const bodyData = ctx.request.body;
    console.log(bodyData); // 获取请求体数据

    ctx.body = { message: '请求成功', data: bodyData };
  }
});

app.listen(3000, () => {
  console.log('服务器启动,监听3000端口...');
});

ctx.response.body===ctx.body : 本质上是一个东西

五. 目录结构优化

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

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()

5 洋葱模型

image.png

洋葱模型,就是将数据顺序传入到多个中间件中,让它们进行处理传递,并利用函数递归的特性,让我们可以在一个中间件内先执行前半部分逻辑,再执行之后的所有中间件的完整逻辑后,再掉转方向继续执行这个中间件的后半部分。

const Koa = require('koa');
const app = new Koa();
 
// 中间件1
app.use((ctx, next) => {
    console.log("<<<1");
    next();
    console.log("1>>>");
});
 
// 中间件 2 
app.use((ctx, next) => {
    console.log("<<<2");
    next();
    console.log("2>>>");
});
 
// 中间件 3 
app.use((ctx, next) => {
    console.log("<<<3");
    next();
    console.log("3>>>");
});
app.listen(8000, '0.0.0.0', () => {
    console.log(`Server is starting`);
});

在 koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。可以通过下图直观看出:

  • 输出的结果
<<<1
<<<2
<<<3
3>>>
2>>>
1>>>
const Koa = require('koa');
const app = new Koa();
 
// 中间件1
app.use(async(ctx, next) => {
    console.log("<<<1");
   await next();
    console.log("1>>>");
});
 
// 中间件 2 
app.use(async(ctx, next) => {
    console.log("<<<2");

   const axios= require('axios')    
   const res= await axios.get('http://www.baidu.com')
    next();
    console.log("2>>>");
});
 
// 中间件 3 
app.use((ctx, next) => {
    console.log("<<<3");
    next();
    console.log("3>>>");
});
app.listen(8000, '0.0.0.0', () => {
    console.log(`Server is starting`);
});

七. 集成 sequlize

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

sequelize ORM 数据库工具
当我们在开发 Node.js 应用程序时,经常需要与数据库进行交互来存储和检索数据。这时候,Sequelize就成了我们的得力助手。Sequelize是一个强大的ORM(对象关系映射)库,它帮助我们以更直观的方式操作数据库,而无需编写复杂的SQL查询语句。

简单来说,ORM是一种编程技术,让我们能够使用面向对象的方法来处理数据库。而Sequelize正是为此而生。它让我们可以用JavaScript代码定义模型和数据之间的关系,就像在创建类和对象一样简单。每个模型类对应数据库中的一张表,而实例则是表中的记录。

Sequelize的好处不胜枚举。首先,它支持多种流行的数据库,比如 PostgreSQL、MySQL、SQLite和MSSQL,让我们在选择数据库时更加灵活。其次,Sequelize允许我们在模型上设置验证规则,确保数据的合法性和完整性。这为我们避免了许多繁琐的数据验证工作,让数据操作更加安全可靠。

查询构建是Sequelize的又一个亮点。它提供了一套流畅的查询构建器,让我们可以轻松地创建复杂的数据库查询。不再需要手动编写冗长的SQL语句,Sequelize可以帮我们处理这些细节,让我们专注于业务逻辑的实现。

如果你担心数据库模式的变更会让开发变得混乱,别担心!Sequelize还支持数据库迁移,让我们可以轻松地管理数据库模式的更新和变更。这样,我们可以在应用程序的生命周期中,保持数据库结构的清晰和一致。

最后,Sequelize还提供了强大的关联功能,让我们可以轻松地定义模型之间的关联关系,比如一对一、一对多、多对多等。这样的话,我们可以轻松地进行跨表查询,从而更加高效地处理复杂的数据需求。

总的来说,Sequelize是一个非常强大且灵活的Node.js ORM库。它简化了数据库操作,提高了开发效率,并且让我们的代码更易于维护。无论是开发小型应用还是大型项目,Sequelize都能成为我们最好的朋友,让我们更加专注于业务逻辑的实现,而不用为繁杂的数据库操作而烦恼。

能让我们以面向对象的方式去操作数据库

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 编写配置文件

  • 第一种方式
const dotenv = require('dotenv')

dotenv.config()
// process.env 根据目录识别的
// ../config/config_default.js
// module.export= {key:value} 第一种方式
module.exports = {
  MYSQL_HOST: 'localhost',
  MYSQL_PORT: '3306',
  MYSQL_USER: 'root',
  MYSQL_PWD: 'root',
  MYSQL_DB: 'auth',
  APP_PORT:'3000'
};

// module.exports=process.env



  • 第二种方式
const dotenv = require('dotenv')

dotenv.config()
// process.env 根据目录识别的
// ../config/config_default.js
// module.export= {key:value} 第一种方式
// module.exports = {
//     MYSQL_HOST: 'localhost',
//     MYSQL_PORT: '3306',
//     MYSQL_USER: 'root',
//     MYSQL_PWD: 'root',
//     MYSQL_DB: 'auth',
//     APP_PORT:'3000'
//   };

module.exports=process.env



APP_PORT=3001

MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PWD=root
MYSQL_DB=auth

八. 创建 User 模型

1 拆分 Model 层

sequelize 主要通过 Model 对应数据表

创建src/model/user.model.js

const { DataTypes } = require('sequelize');
const seq = require('../db/db_sequ');

const User = seq.define('zd_user', {
  user_name: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  is_admin: {
    type: DataTypes.INTEGER,
    defaultValue: 0,
  },
}, {
  tableName: 'zd_user',
  timestamps: false,
});

module.exports = User;
  • 对应sql
-- auth.zd_user definition

CREATE TABLE `zd_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `is_admin` int(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO auth.zd_user
(id, user_name, password, is_admin)
VALUES(5, 'root', 'root123', 0);

九. 添加用户入库

所有数据库的操作都在 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')

// 发起请求 -》 controller -》 service --》 model -》 操作数据 -》 
//使用了 sequelize 完成对数据的保存操作
class UserController {

  // 方法被 async 修饰了,调用处必须加上await
  async register(ctx, next) {
    // 1. 获取数据
    // console.log(ctx.request.body)
    
    const { username, password } = ctx.request.body

    // 合法性
    if (!username || !password) {
      console.error('用户名或密码为空', ctx.request.body)
      ctx.status = 400
      ctx.body = {
        code: '10001',
        message: '用户名或密码为空',
        result: '',
      }
      return
    }
    // 合理性
    if (await getUerInfo({ user_name:username })) {
      ctx.status = 409
      ctx.body = {
        code: '10002',
        message: '用户已经存在',
        result: '',
      }
      return
    }
    // 2. 操作数据库
    const res = await createUser(username, 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/user.model')

// Service 处理了业务逻辑
class UserService {
  async createUser(user_name, password) {
    const userData = { user_name, password };
    const res = await User.create(userData);
    return res;
  }

  // 查询 用户名是否存在, true
  async getUerInfo({ id, user_name, password, is_admin }) {
    const whereOpt = {} 
    console.log('user_name:',user_name)
    id && Object.assign(whereOpt, { id })
    user_name && Object.assign(whereOpt, { user_name })

    // User.findOne 查询 用户名是否存在
    const res = await User.findOne({
      attributes: ['id', 'user_name', 'password', 'is_admin'],
      where: whereOpt,
    })
    

    return res ? res.dataValues : null
  }

}

module.exports = new UserService()

十一. 拆分中间件

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

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)
// 注册接口
router.post('/register', userValidator, verifyUser, register)

十二. 加密

bcryptjs 是Node.js中比较简洁好用的一款第三方加盐(salt)加密包,并且支持跨平台的特性,用于实现在Node环境下重要资源的加密。
由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。

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

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()
}
const verifyUser = async (ctx, next) => {
  const { username } = ctx.request.body

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

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



  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)

十四. 用户的认证

官方介绍:JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对签名。
https://jwt.io/

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

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

1 颁发 token

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image.png

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,
}
  • 登录生成 toekn 后, 后续任意接口都需要携带 token 进行 访问 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2) 改写 router

  • 具体操作之前, 验签
// 修改密码接口
router.patch('/', auth, (ctx, next) => {
  console.log(ctx.state.user)
  ctx.body = '修改密码成功'
})

十五 接口练习

  • 安装依赖
{
  "name": "test",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "dotenv": "^16.3.1",
    "koa": "^2.14.2",
    "koa-router": "^11.0.2",
    "mysql2": "^2.2.5",
    "nodemon": "^3.0.1"
  },
  "scripts": {
    "dev": "nodemon ./main.js"
  }
}

数据库名称 test_spiritmark_db
数据库用户 admin_spiritmark
数据库密码 55726955c65bd84c
数据库地址 mysql.sqlpub.com:3306
注册邮箱 liuluchang@cathay-ins.com.cn
注意 密码只显示一次,请注意保存。
为保障数据库运行安全,密码严禁泄漏到公共环境!!!(如:csdn、zhihu、github等)发现即永久锁定。

  • 测试sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT NOT NULL
);

INSERT INTO users (name, age) VALUES
('John Doe', 25),
('Jane Smith', 30),
('Mike Johnson', 35);

select * from users;
  • 启动应用
const Koa = require('koa');
const Router = require('koa-router');
const mysql = require('mysql2/promise');

const app = new Koa();
const router = new Router();

// 创建数据库连接池
const pool = mysql.createPool({
  host: 'mysql.sqlpub.com',
  user: 'admin_spiritmark',
  port:3306,
  password: '55726955c65bd84c',
  database: 'test_spiritmark_db',
});

// 添加数据
router.post('/api', async (ctx) => {
  const { name, age } = ctx.request.body;
  try {
    const [result] = await pool.query('INSERT INTO users (name, age) VALUES (?, ?)', [name, age]);
    ctx.body = result;
  } catch (error) {
    ctx.status = 500;
    ctx.body = error.message;
  }
});

// 删除数据
router.del('/api/:id', async (ctx) => {
  const { id } = ctx.params;
  try {
    const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]);
    ctx.body = result;
  } catch (error) {
    ctx.status = 500;
    ctx.body = error.message;
  }
});

// 更新数据
router.put('/api/:id', async (ctx) => {
  const { id } = ctx.params;
  const { name, age } = ctx.request.body;
  try {
    const [result] = await pool.query('UPDATE users SET name = ?, age = ? WHERE id = ?', [name, age, id]);
    ctx.body = result;
  } catch (error) {
    ctx.status = 500;
    ctx.body = error.message;
  }
});

// 查询数据
router.get('/api', async (ctx) => {
  const page = parseInt(ctx.query.page) || 1;
  const size = parseInt(ctx.query.size) || 10;
  const offset = (page - 1) * size;

  try {
    const [rows] = await pool.query('SELECT * FROM users LIMIT ?, ?', [offset, size]);
    ctx.body = rows;
  } catch (error) {
    ctx.status = 500;
    ctx.body = error.message;
  }
});

// 启动应用程序
app.use(router.routes()).use(router.allowedMethods());

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

image.png

  • 封装优化
const Koa = require('koa');
const Router = require('koa-router');
const mysql = require('mysql2/promise');

const app = new Koa();
const router = new Router();

// 创建数据库连接池
const createPool = () => {
  return mysql.createPool({
    host: 'mysql.sqlpub.com',
    user: 'admin_spiritmark',
    port:3306,
    password: '55726955c65bd84c',
    database: 'test_spiritmark_db',
  });
};

// 添加数据
const addUser = async (name, age) => {
  const pool = createPool();
  try {
    const [result] = await pool.query('INSERT INTO users (name, age) VALUES (?, ?)', [name, age]);
    return result;
  } catch (error) {
    throw new Error(error.message);
  } finally {
    pool.end();
  }
};

// 删除数据
const deleteUser = async (id) => {
  const pool = createPool();
  try {
    const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]);
    return result;
  } catch (error) {
    throw new Error(error.message);
  } finally {
    pool.end();
  }
};

// 更新数据
const updateUser = async (id, name, age) => {
  const pool = createPool();
  try {
    const [result] = await pool.query('UPDATE users SET name = ?, age = ? WHERE id = ?', [name, age, id]);
    return result;
  } catch (error) {
    throw new Error(error.message);
  } finally {
    pool.end();
  }
};

// 查询数据
const getUsers = async (page = 1, size = 10) => {
  const pool = createPool();
  const offset = (page - 1) * size;
  try {
    const [rows] = await pool.query('SELECT * FROM users LIMIT ?, ?', [offset, size]);
    return rows;
  } catch (error) {
    throw new Error(error.message);
  } finally {
    pool.end();
  }
};

// 添加数据
router.post('/api', async (ctx) => {
  const { name, age } = ctx.request.body;
  try {
    const result = await addUser(name, age);
    ctx.body = result;
  } catch (error) {
    ctx.status = 500;
    ctx.body = error.message;
  }
});

// 删除数据
router.del('/api/:id', async (ctx) => {
  const { id } = ctx.params;
  try {
    const result = await deleteUser(id);
    ctx.body = result;
  } catch (error) {
    ctx.status = 500;
    ctx.body = error.message;
  }
});

// 更新数据
router.put('/api/:id', async (ctx) => {
  const { id } = ctx.params;
  const { name, age } = ctx.request.body;
  try {
    const result = await updateUser(id, name, age);
    ctx.body = result;
  } catch (error) {
    ctx.status = 500;
    ctx.body = error.message;
  }
});

// 查询数据
router.get('/api', async (ctx) => {
  const page = parseInt(ctx.query.page) || 1;
  const size = parseInt(ctx.query.size) || 10;

  try {
    const rows = await getUsers(page, size);
    ctx.body = rows;
  } catch (error) {
    ctx.status = 500;
    ctx.body = error.message;
  }
});

// 启动应用程序
app.use(router.routes()).use(router.allowedMethods());

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

十六 koa集成 模板引擎

EJS 的使用

    1. 使用模板引擎
# 在项目中下载koa-views包
npm i koa-views
# 在项目中下载ejs包
npm i ejs
    1. 入口index.js
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
// 1. 引入koa-views
const views = require('koa-views');
// 2. 配置模板引擎中间件:views()第一个参数是视图模板所在的路径,第二个参数是应用ejs模板引擎
app.use(views('views', { extension: 'ejs' })); // 若这样配置,模板的后缀名是.ejs
//app.use(views('views', { map: { html: 'ejs' } })); // 若这样配置,模板的后缀名是.html
 
// 我们需要在每个路由的render中都渲染一个公共的数据
// 写一个中间件配置公共信息
app.use(async (ctx, next) => {
    // 公共的数据要放在ctx.state中
    ctx.state = {
        userinfo: '张三',
        age: '18',
    }
    await next()
})
 
router.get('/', async (ctx, next) => {
    let title = '你好ejs'
    // 3. 异步渲染模板
    await ctx.render('index.ejs', {
        // 绑定数据
        title: title
    })
})
 
router.get('/news', async (ctx, next) => {
    let arr = ['111111', '22222', '444444']
    let content = "<h2>这是一个h2</h2>"
    let num = 123
    await ctx.render('news.ejs', {
        list: arr,
        content: content,
        num: num
    })
});
 
app.use(router.routes());
app.use(router.allowedMethods());
 
app.listen(3000, () => {
    console.log('starting at port 3000');
});
  • /views/index.ejs:
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ejs模板引擎</title>
</head>
<body>
    <%- include('./public/header.ejs') %>
    这是一个ejs模板引擎
    <h2><%=title%>-----<%=userinfo%></h2>
</body>
</html>
  • /views/news.ejs:
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ejs数据绑定</title>
</head>
<body>
    <h2>引入外部模块</h2>
    <%- include('./public/header.ejs') %>
 
    <h2>ejs循环渲染数据</h2>
    <ul>
        <%for(var i=0;i<list.length;i++){%>
            <li><%=list[i]%></li>
        <%}%>
    </ul>
 
    <h2>绑定html数据</h2>
    <%-content%>
 
    <h2>条件判断</h2>
    <% if(num>24){ %>
        大于24
    <%} else{ %> 
        小于24
    <%} %>
 
    <h2>公共数据</h2>
    <%=userinfo%>
</body>
</html>

art-template

原始语法和ejs一模一样都用<% %>,标准语法用的是{{ }},这里代码示例用的是标准语法。

  • 安装所需包
npm install --save art-template
npm install --save koa-art-template
  • 入口index.js
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
const path = require('path');
// 1. 引入koa-art-template
const render = require('koa-art-template');
// 2. 配置art-template模板引擎
render(app, {
    root: path.join(__dirname, 'views'), // 视图的位置
    extname: '.html', // 后缀名
    debug: process.env.NODE_ENV !== 'production' // 是否开启调试模式
});
 
// 我们需要在每个路由的render中都渲染一个公共的数据
// 写一个中间件配置公共信息
app.use(async (ctx, next) => {
    // 公共的数据要放在ctx.state中
    ctx.state = {
        userinfo: '张三',
        age: '18',
    }
    await next()
})
 
router.get('/', async (ctx, next) => {
    let title = '你好koa-art-template'
    // 3. 异步渲染模板
    await ctx.render('index', {
        // 绑定数据
        title: title
    })
})
 
router.get('/news', async (ctx, next) => {
    let arr = ['111111', '22222', '444444']
    let content = "<h2>这是一个h2</h2>"
    let num = 123
    await ctx.render('news', {
        list: arr,
        content: content,
        num: num
    })
});
 
app.use(router.routes());
app.use(router.allowedMethods());
 
app.listen(3000, () => {
    console.log('starting at port 3000');
});
  • /views/news.html:
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数据绑定</title>
</head>
<body>
    <h2>引入外部模块</h2>
    {{include './public/header.html'}}
 
    <h2>绑定数据</h2>
    {{list.name}}
 
    <h2>公共数据</h2>
    {{userinfo}}
 
    <h2>绑定html数据</h2>
    {{@list.h}}
 
    <h2>条件判断</h2>
    {{if num>20}}<span>大于20</span>{{else}}<span>小于20</span>{{/if}}
 
    <h2>循环渲染数据</h2>
    <ul>
        {{each list.data}}
            <li>{{$index}}--{{$value}}</li>
        {{/each}}
    </ul>
</body>
</html>

http://www.kler.cn/a/152499.html

相关文章:

  • MySQL的安装步骤教程以及基本操作--详细讲解
  • ChatGPT 问世一周年之际,开源大模型能否迎头赶上?
  • 口碑最好超声波清洗机有哪些?2023年超声波清洗机排行榜
  • 数据库安全运维系统厂家在深圳的有哪些?咨询电话多少?
  • java基础面试题(二)
  • Laravel修改默认的auth模块为md5(password+salt)验证
  • 电源控制系统架构(PCSA)之系统控制处理器组件
  • 企业软件手机app定制开发趋势|小程序网站搭建
  • Maven的安装和使用
  • opencv读取二进制灰度图并显示
  • 15 网关实战: 微服务集成Swagger实现在线文档
  • RPC与HTTP的详细比较
  • maven 基础
  • 大屏适配方案(vw、vh)
  • 抑制过拟合——从梯度的角度看LayerNorm的作用
  • Vue3 配置自动导入的步骤
  • Android 11.0 软硬键盘同时使用的兼容(软键盘与内置物理键盘共存)
  • CSS新手入门笔记整理:CSS列表样式
  • 1.自动化运维工具Ansible的安装
  • Linux CentOS7 安装Docker