Skip to the content.

写在前面

上篇中,我们做了基础的用户增删改查 这一篇我们加入登录验证 项目地址:https://github.com/JohnnyLuv/nodejs-easy-demo-202003/tree/jwt

交互流程

用户发起登录,通过数据库判断登录结果,成功后签发 token 客户端本地保存 token,访问需要登录权限的接口时,在请求头中携带 token 服务器验证 token是否有效,并做相应返回 这种验证机制就是 jwt🦄

|--------|                             |---------|
|        |         user login          |         |
|        |---------------------------->|         |
|        |<----------------------------|         |
|        |       response token        |         |
|        |                             |         |
|        |                             |         |
|        |  request with valid token   |         |
| client |---------------------------->| service |
|        |<----------------------------|         |
|        |        response 200         |         |
|        |                             |         |       
|        |                             |         |
|        |   request with bad token    |         |
|        |---------------------------->|         |
|        |<----------------------------|         |
|        |        response 401         |         |
|--------|                             |---------|

什么是jwt

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

服务端-生成token

controller 中暴露 login 接口

// controller/userController.js

const Router = require('koa-router'),
  router = new Router()
router.prefix('/api') // 添加请求前缀

const userModule = require('../module/userModule')

// 用户登录
router.post('/login', async ctx => {
  const response = await userModule.login(ctx)
  ctx.body = response
})

module.exports = router

安装 jsonwebtoken 来进行加密和签名

$ yarn add jsonwebtoken

module 中添加 login 的业务逻辑

// module/userModule.js

const DB = require('./db')
const jwt = require('jsonwebtoken')


/**
 * 用户登录
 * @param {Object} ctx 
 */
const login = async ctx => {
  const query = ctx.request.body
  // console.log(query)

  let response = {}
  switch (true) {
    case !query.username:
      response = { status: 201, msg: 'username 必填' }
      break
    case !query.password:
      response = { status: 201, msg: 'password 必填' }
      break
    default:
      const data = await DB.find('user', { username: query.username })
      if (query.username === data[0].username && query.password === data[0].password) {
        // 用户名和密码匹配,登录成功,生成token
        const token = jwt.sign(
          // 携带自定义参数
          {
            _id: data[0]._id,
            username: data[0].username,
          },
          'johnny_jwt_secret', // 私钥,加密和解密时,要保证一致
          { expiresIn: '1h' } // 过期时间
        )
        response = {
          status: 200,
          data: { ...data[0], token: `Bearer ${token}` },
          msg: '登录成功',
        }
      } else {
        response = { status: 201, msg: '账号或密码错误' }
      }
      break
  }
  return response
}


module.exports = {
  login
}

客户端-获取token

客户端发起登录请求,登录成功后获取 token 并保存在本地

// views/login.html

new Vue({
  el: "#root",
  data: {
    username: "",
    password: "",
  },
  methods: {
    onSubmit() {
      const params = {
        username: this.username,
        password: this.password,
      }
      // console.log(params)
      axios.post('/api/login', params).then(res => {
        alert('登录成功', res.username)
        localStorage.setItem('token', res.token)
        location.href = '/'
      })
    }
  }
})

配置axios请求拦截,向请求头注入 token

// assets/js/util.js

// 客户端请求拦截器
axios.interceptors.request.use(
  config => {
    // Do something before request is sent
    const token = window.localStorage.getItem('token')
    token && (config.headers.Authorization = token) // 在请求头中设置 Authorization:token
    return config
  },
  error => {
    // Do something with request error
    return Promise.reject(error)
  }
)

// 服务端响应拦截器
axios.interceptors.response.use(
  response => {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    switch (response.data.status) {
      case 200:
        return response.data.data
      case 401:
        // 状态码401 token失效需要重新登录重新获取
        alert(response.data.msg)
        location.href = '/login'
        // return Promise.reject(response.data)
      default:
        alert(response.data.msg)
        return Promise.reject(response.data)
    }
  },
  error => {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    alert('网路异常')
    return Promise.reject(error)
  }
)

服务端-验证token

安装 koa-jwt 进行解密,虽然 jsonwebtoken 自带解密方法,但是比较繁琐

$ yarn add koa-jwt

在入口文件添加鉴权验证,需要注意的是中间件的顺序,jwt鉴权模块要放在所有路由模块之前

// app.js

const Koa = require('koa'),
  app = new Koa()

const Router = require('koa-router'),
  router = new Router()

// 模板引擎
const views = require('koa-views')
app.use(views('views'))

// request中间件
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())

// 设置静态文件目录
const static = require('koa-static')
app.use(static(__dirname))


// jwt鉴权
app.use((ctx, next) => {
  return next().catch(err => {
    if (err.status === 401) {
      ctx.status = 200
      ctx.body = {
        status: 401,
        msg: '登录失效'
      }
    } else {
      return next()
    }
  })
})
const jwt = require('koa-jwt')
app.use(jwt({ secret: 'johnny_jwt_secret' }).unless({ path: [ '/', '/login', /^\/user\//, '/api/login'] }))


// 页面路由注册
const viewRouter = require('./router')
app.use(viewRouter.routes()).use(router.allowedMethods())

// 接口路由注册
const userApi = require('./controller/userController')
app.use(userApi.routes()).use(router.allowedMethods())


app.listen(3000, () => {
  console.log('service running at http://localhost:3000/')
})

文档和攻略

  1. 《JSON Web Token 入门教程》阮一峰:www.ruanyifeng.com/
  2. jsonwebtoken 文档:github.com/auth0/node-jsonwebtoken
  3. koa-jwt 文档:https://github.com/koajs/jwt

完结

🎊撒花,以上就是 jwt 的核心部分,可以到 git 仓库 jwt 分支查看全部代码