10

What is Koa2

The new generation Node.js web framework created by the original team of Express, its code is very simple, it does not provide routing, static services, etc. like Express, it is to solve the Node problem (simplify the operation in Node) and replace it , which itself is a simple middleware framework that needs to be used with each middleware

Documentation

Chinese document (wild)

Simplest Koa server

 const Koa = require('koa')

const app = new Koa()

app.use((ctx) => {
  ctx.body = 'Hello World'
})

app.listen(3000, () => {
  console.log('3000端口已启动')
})

onion model

洋葱模型

This is Koa's onion model

Take a look at what Express middleware looks like:

Express的中间件

The request (Request) runs through each middleware directly in turn, and finally returns the response (Response) through the request processing function. Let's take a look at what Koa's middleware looks like:

koa的中间件

It can be seen that the Koa middleware does not complete its mission after the request is passed like the Express middleware; on the contrary, the execution of the middleware is clearly divided into two stages. Let's see what Koa middleware looks like

Definition of Koa Middleware

Koa's middleware is a function like this:

 async function middleware(ctx, next) {
    // 先做什么
    await next()
    // 后做什么
}

The first parameter is the Koa Context, which is the content passed by the green arrow running through the middleware and the request processing function in the above figure, which encapsulates the request body and the response body (in fact, there are other attributes), which can be passed through ctx.request and ctx.response to get, the following are some commonly used attributes:

 ctx.url // 相当于 ctx.request.url
ctx.body // 相当于 ctx.response.boby
ctx.status // 相当于 ctx.response.status
For more Context properties, please refer to the Context API documentation

The second parameter of the middleware is next function: used to transfer control to the next middleware. But the essential difference between it and Express's next function is that Koa's next function returns a Promise . After the Promise enters the fulfilled state (Fulfilled), it will go to the middle of the execution. The code for the second stage in the file.

What are the common middleware

Routing middleware - koa-router or @koa/router

Download npm package

 npm install koa-router --save
Some tutorials use @koa/router , and now these two libraries are maintained by the same person and the code is the same. i.e. koa-router === @koa/router (Written as of Aug 23, 2021)

NPM package address: koa-router , @koa/router

how to use

Create the controllers directory in the root directory to store the code related to the controller. The first is HomeController, create controllers/home.js, the code is as follows:

 class HomeController {
  static home(ctx) {
    ctx.body = 'hello world'
  }
  static async login(ctx) {
    ctx.body = 'Login Controller'
  }
  static async register(ctx) {
    ctx.body = 'Register Controller'
  }
}

module.exports = HomeController;

implement routing

Then create the routes folder to mount the controller to the corresponding route and create home.js

 const Router = require('koa-router')
const { home, login, register } = require('../controllers/home')

const router = new Router()

router.get('/', home)
router.post('/login', login)
router.post('/register', register)

module.exports = router

register route

Create index.js in routes, and put all routes into routes in the future. The purpose of creating index.js is to make the structure more tidy. index.js is responsible for the registration of all routes, and its sibling files are responsible for their own routes.

 const fs = require('fs')
module.exports = (app) => {
  fs.readdirSync(__dirname).forEach((file) => {
    if (file === 'index.js') {
      return
    }
    const route = require(`./${file}`)
    app.use(route.routes()).use(route.allowedMethods())
  })
}

Note: the role of allowedMethods

  1. Responds to the option method, telling it which request methods it supports
  2. 405 (not allowed) and 501 (not implemented) are returned accordingly

Note: It can be seen that the usage of @koa/router is basically the same as that of Express Router.

import route

Finally, we need to register the router as middleware, create a new index.js , and write the code as follows:

 const Koa = require('koa')
const routing = require('./routes')

// 初始化 Koa 应用实例
consr app = new Koa()

// 注册中间件
// 相应用户请求
routing(app)

// 运行服务器
app.listen(3000);

Test it with postman

测试路由

other middleware

  • koa-bodyparser - request body parsing
  • koa-static - serve static resources
  • @koa/cors - cross domain
  • koa-json-error - handle errors
  • koa-parameter - parameter verification
 cnpm i koa-bodyparser -S 
cnpm i koa-static -S
cnpm i @koa/cors -S
cnpm i koa-json-error -S
cnpm i koa-parameter -S
 const path = require('path')
const Koa = require('koa')
const bobyParser = require('koa-bodyparser')
const koaStatic = require('koa-static')
const cors = require('@koa/cors')
const error = require('koa-json-error')
const parameter = require('koa-parameter')
const routing = require('./routes')

const app = new Koa()

app.use(
  error({
    postFormat: (e, { stack, ...rest }) =>
      process.env.NODE_ENV === 'production' ? rest : { stack, ...rest },
  }),
)
app.use(bobyParser())
app.use(koaStatic(path.join(__dirname, 'public')))
app.use(cors())
app.use(parameter(app))
routing(app)

app.listen(3000, () => {
  console.log('3000端口已启动')
})

Implement JWT authentication

JSON Web Token (JWT) is a popular RESTful API authentication scheme

Install the relevant npm packages first

 cnpm install koa-jwt jsonwebtoken -S

Create config/index.js to store the JWT Secret constant, the code is as follows:

 const JWT_SECRET = 'secret'

module.exports = {
  JWT_SECRET,
}

Some routes we want only logged in users to have access to (protected routes), while others are accessible to all requests (unprotected routes). In Koa's onion model, we can do it like this:

加入JWT后的洋葱模型

It can be seen that all requests can directly access unprotected routes, but protected routes are placed behind the JWT middleware, we need to create a few more files to do JWT experiments

We know that the so-called user (users) is the most common route that requires authentication, so we now create user.js in controllers and write the following code:

 class UserController {
  static async create(ctx) {
    ctx.status = 200
    ctx.body = 'create'
  }
  static async find(ctx) {
    ctx.status = 200
    ctx.body = 'find'
  }
  static async findById(ctx) {
    ctx.status = 200
    ctx.body = 'findById'
  }
  static async update(ctx) {
    ctx.status = 200
    ctx.body = 'update'
  }
  static async delete(ctx) {
    ctx.status = 200
    ctx.body = 'delete'
  }
}

module.exports = UserController

Register JWT middleware

The user's additions, deletions, changes and checks are arranged, and the semantics are obvious. Next, we create user.js in the routes file, and here is the code related to the users route:

 const Router = require('koa-router')
const jwt = require('koa-jwt')
const {
  create,
  find,
  findById,
  update,
  delete: del,
} = require('../controllers/user')

const router = new Router({ prefix: '/users' })
const { JWT_SECRET } = require('../config/')

const auth = jwt({ JWT_SECRET })

router.post('/', create)
router.get('/', find)
router.get('/:id', findById)
router.put('/:id', auth, update)
router.delete('/:id', auth, del)

module.exports = router

In summary, the home.js under the routes file does not need the protection of JWT middleware, and the update and deletion in user.js need the protection of JWT

Test it, you can see that the JWT has worked

测试JWT

So far, we have completed the verification of JWT, but the premise of verification is to issue JWT first, how to issue it, I will give you a signed token when you log in, and bring the token in the request header when you want to update/delete , I was able to verify...

Login is involved here. Let's pause for a while and supplement the knowledge of the database to make the project more complete.

Mongoose joins the battle

If you want to do a complete project, the database is essential. The NoSql database that matches Node is better, which is represented by Mongodb. Of course, if we want to use this database, we need to follow the corresponding library, and this library is mongoose

download mongoose

 cnpm i mongoose -S

Connect and configure

Add the connectionStr variable in config/index.js to represent the database address of the mongoose connection

 const JWT_SECRET = 'secret'
const connectionStr = 'mongodb://127.0.0.1:27017/basic'

module.exports = {
  JWT_SECRET,
  connectionStr,
}

CREATE db/index.js

 const mongoose = require('mongoose')
const { connectionStr } = require('../config/')

module.exports = {
  connect: () => {
    mongoose.connect(connectionStr, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    })

    mongoose.connection.on('error', (err) => {
      console.log(err)
    })

    mongoose.connection.on('open', () => {
      console.log('Mongoose连接成功')
    })
  },
}

Enter the main file index.js , modify the configuration and start

 ...
const db = require('./db/')
...

db.connect()

Start the service npm run serve , that is nodemon index.js , you can see that mongoose has been connected successfully

nodemon

Create a data model definition

Create the models directory in the root directory to store the data model definition file, and create User.js in it to represent the user model. The code is as follows:

 const mongoose = require('mongoose')

const schema = new mongoose.Schema({
  username: { type: String },
  password: { type: String },
})

module.exports = mongoose.model('User', schema)

For details, you can look at the article Mongoose . Here we look at the behavior. The above code indicates that a data object is created for the operator to operate the database.

Operate the database in the Controller

Then you can add, delete, modify, and query data in the Controller. First we open constrollers/user.js

 const User = require('../models/User')

class UserController {
  static async create(ctx) {
    const { username, password } = ctx.request.body
    const model = await User.create({ username, password })
    ctx.status = 200
    ctx.body = model
  }
  static async find(ctx) {
    const model = await User.find()
    ctx.status = 200
    ctx.body = model
  }
  static async findById(ctx) {
    const model = await User.findById(ctx.params.id)
    ctx.status = 200
    ctx.body = model
  }
  static async update(ctx) {
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
}

module.exports = UserController

In the above code,

  • User.create({xxx}): Create a data in the User table
  • User.find(): View all the data in the User table
  • User.findById(id): View one of the User tables
  • User.findByIdAndUpdate(id, body): Update one of the data in the User table
  • User.findByIdAndDelete(id): delete one of the data in the User table

The above is the addition, deletion and modification of the database

With salt

For this, we need to encrypt the password, without it, it is safe.

When you check into the database, you can see the password, which means that the data is open to the developer, and the developer can do anything with the user's account password, which is not allowed.

数据库中的用户表

Download npm package - bcrypt

 cnpm i bcrypt --save

Let's go to models/User.js and transform it

 ...
const schema = new mongoose.Schema({
  username: { type: String },
  password: {
    type: String,
    select: false,
    set(val) {
      return require('bcrypt').hashSync(val, 10)
    },
  },
})
...

Add select: false is invisible, set(val) encrypts the value, let's test it

创建李四

It can be seen that the password is encrypted. Even in the database, the user's password cannot be seen. Does the password entered by the user enter such a string of passwords? Obviously not, if the user enters it, we have to verify it. For example, we login

We enter the constrollers/home file and transform it,

 ...
class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body
    const user = await User.findOne({ username }).select('+password')
    const isValid = require('bcrypt').compareSync(password, user.password)
    ctx.status = 200
    ctx.body = isValid
  }
  ...
}
  • User.findOne({ username }) can find data without password, because we artificially set select to false. If you want to see it, add select('+password')
  • require('bcrypt').compareSync(password, user.password) Authenticate the plaintext password entered by the user with the encrypted password in the database, true is correct, false is incorrect password

Back to JWT

Issue JWT Token in Login

We need to provide an API port so that users can get the JWT Token. The most suitable one is of course the login interface /login , open controllers/home.js , and implement it in the login controller The logic of issuing JWT Token, the code is as follows:

 const jwt = require('jsonwebtoken')
const User = require('../models/User')

const { JWT_SECRET } = require('../config/')

class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body

    // 1.根据用户名找用户
    const user = await User.findOne({ username }).select('+password')
    if (!user) {
      ctx.status = 422
      ctx.body = { message: '用户名不存在' }
    }
    // 2.校验密码
    const isValid = require('bcrypt').compareSync(password, user.password)
    if (isValid) {
      const token = jwt.sign({ id: user._id }, JWT_SECRET)
      ctx.status = 200
      ctx.body = token
    } else {
      ctx.status = 401
      ctx.body = { message: '密码错误' }
    }
  }
  ...
}

In login , we first query the corresponding user according to the user name (the name field in the request body), if the user does not exist, return 401 directly; if it exists, then pass (bcrypt').compareSync To verify the plaintext password in the request body password Whether it is consistent with the encrypted password stored in the database, if it is consistent, pass jwt.sign the Token, if it is inconsistent, still 401 is returned.

Add access control in User controller

After the middleware and issuance of the Token are completed, the final step is to verify the user's Token in a suitable place to confirm whether it has sufficient permissions. The most typical scenario is that when updating or deleting a user, we want to make sure that the user himself is operating . Open controllers/user.js

 const User = require('../models/User')

class UserController {
  ...
  static async update(ctx) {
    const userId = ctx.params.id
    if (userId !== ctx.state.user.id) {
      ctx.status = 403
      ctx.body = {
        message: '无权进行此操作',
      }
      return
    }
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    const userId = ctx.params.id

    if (userId !== ctx.state.user.id) {
      ctx.status = 403
      ctx.body = { message: '无权进行此操作' }
      return
    }

    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
}

module.exports = UserController

Add some users and log in, add the Token to the request header, use DELETE to delete the user, you can see that the status code becomes 204, the deletion is successful

删除用户操作

Assertion Handling

When logging in, updating user information, and deleting users, we need if else to judge. This looks stupid. If we can handle it with assertions, the code will look much more elegant. At this time http-assert came out

 // constrollers/home.js
...
const assert = require('http-assert')


class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body
    // 1.根据用户名找用户
    const user = await User.findOne({ username }).select('+password')
    // if (!user) {
    //   ctx.status = 401
    //   ctx.body = { message: '用户名不存在' }
    // }
    assert(user, 422, '用户不存在')
    // 2.校验密码
    const isValid = require('bcrypt').compareSync(password, user.password)
    assert(isValid, 422, '密码错误')
    const token = jwt.sign({ id: user._id }, JWT_SECRET)
    ctx.body = { token }
  }
   ...
}

Similarly, process controllers/user

 ...
  static async update(ctx) {
    const userId = ctx.params.id
    assert(userId === ctx.state.user.id, 403, '无权进行此操作')
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    const userId = ctx.params.id
    assert(userId === ctx.state.user.id, 403, '无权进行此操作')
    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
...

The code looks clean and fresh

parameter verification

We added a middleware before -- koa-parameter , we just registered this middleware, but it is not used, we need to judge that the data type of username and password is String type and required when creating a user, Enter controllers/user.js add the code as follows:

 ...
class UserController {
  static async createUser(ctx) {
    ctx.verifyParams({
      username: { type: 'string', required: true },
      password: { type: 'string', required: true },
    })
    const { username, password } = ctx.request.body
    const model = await User.create({ username, password })
    ctx.status = 200
    ctx.body = model
  }
  ...
}

Github address: koa-basic

References

Time for a cup of tea, get started with Koa2 + MySQL development


山头人汉波
391 声望554 粉丝