头图

项目环境:node.js + koa + koa-jwt + jsonwebtoken + mysql + sequelize

1.环境安装

npm install koa -S
npm install koa-router -S
npm install koa-bodyparser -S
npm install koa-jwt jsonwebtoken -S
npm install --save mysql2
npm install --save sequelize
npm install require-directory -S  // 使用它来加载路由文件夹下的所有 router,实现路由自动注册
npm install koa-session -S // 存储用户登录信息

require-directory 的使用

// app.js
const requireDirectory = require('require-directory')

// 需要放在路由之后
// require-directory 实现路由自动注册
// 第一个参数固定是 module,第二个参数是要注册的 router 的相对路径,第三个参数是注册每个路由之前执行的业务代码
const modules = requireDirectory(module, './routes', { visit: whenLoadModule })
function whenLoadModule (obj) {
  // 判断当前对象是否是一个 Router,这种判断方式只适用于导出时没有使用大括号的方式,
  if (obj instanceof router) {
    app.use(obj.routes() , obj.allowedMethods())
  }
}

koa-session 的使用

// app.js
const session = require('koa-session')

// koa-session
app.keys = ['some secret hurr']
const CONFIG = {
  key:'koa:sess',    /*cookie key (default is koa:sess)*/
  maxAge:86400000,   /*cookie 的过期时间maxAge in ms (default is 1 days)*/
  overwrite:true,   /*是否可以overwrite (默认default true)*/
  httpOnly:true,    /*cookie 是否只有服务器端可以访问httpOnly or not (default true)*/
  signed:true,    /*默认签名*/
  rolling:false,    /*在每次请求时强行设置cookie,这将重置cookie过期时间(默认:false)*/
  renew:true,    /*当cookie快过期时请求,会重置cookie的过期时间*/
}
app.use(session(CONFIG, app))

// 设置 session
ctx.session.username = ‘user’
// 获取 session
ctx.session.username

2.数据库表设计

user 用户表

字段类型允许为空
id用户表主键,自增id
username用户名
password密码
ridrole 表外键

role 角色表

字段类型允许为空默认值
id角色表主键,自增id
name角色名称
description角色描述
menu角色所属菜单[]
permission角色所属权限[]

permission 权限表

字段类型允许为空默认值
id权限表主键,自增id
name接口名称
path接口地址
type接口类型
basename上级节点名称
basepath上级节点地址
show是否显示1
enable是否启用1

node.js 中的 token 验证--以登录为例
目的:在服务端实现客户端请求携带 token 的验证
方法:使用第三方JWT模块生成并验证 token

解析:

权限认证 JWT (对所有路由进行拦截,排除登录、注册、发送短信等路由)
前端:用户登录输入用户名和密码,请求服务器端接口
服务端:验证用户名和密码正确后,生成 token 返回给前端

  • 全局拦截路由设置:全局路由拦截需要放在其他路由之前
  • 首先判断是否有排除的登录、注册等路由,对这些路由进行放行。否则——判断 headers 中是否存在 authorization,如果 authorization 值为 undefined,提示没有访问权限。否则——验证 token 是否等于当前登录用户的用户名,等于的话,再判断此用户的角色表中的 permission 字段是否存在 ctx.url ,是的话 放行 next(),否则提示未授权
    前端存储 token ,并在后面请求中把 token 带在请求头中传给服务器

    3.使用

    项目目录结构

mysql 和 sequelize 的配置

config/mysql.js

// Mysql 数据库的基本配置信息
const config = {
  database: 'jwt', // 使用的是哪个数据库
  username: 'root', // 用户名
  password: '1****6', // 密码
  host: 'localhost', // 主机名
  port: 3306 // 端口号,Mysql 默认为 3306
}

module.exports = config

config/db.js

const { Sequelize } = require('sequelize')
const config = require('./mysql')

const sequelize = new Sequelize(config.database, config.username, config.password, {
  host: config.host,
  dialect: 'mysql',
  pool: {
    max: 5, // 连接池中最大连接数量
    min: 0, // 连接池中最小连接数量
    idle: 10000 // 如果一个线程 10 秒钟内没有被使用过的话,就释放线程
  },
  define: {
    timestamps: false, // 不自动创建时间字段
    freezeTableName: true  // 参数停止 Sequelize 执行自动复数化. 这样,Sequelize 将推断表名称等于模型名称,而无需进行任何修改
  }
})

//测试数据库链接
sequelize
  .authenticate().then(() => {
    console.log("数据库连接成功");
  })
  .catch((err) => {
    //数据库连接失败时打印输出
    console.error(err);
    throw err;
  });
  
// sequelize.sync({ force: false })

module.exports = sequelize

models/model.js

const { DataTypes } = require('sequelize')
const sequelize = require('../config/db')

// user 模型
const User = sequelize.define('user', {
  // 在这里定义模型属性
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true,
    allowNull: false
  },
  username: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '用户名'
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '密码'
  }
})

const Role = sequelize.define('role', {
  // 在这里定义模型属性
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true,
    allowNull: false
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '角色名称'
  },
  description: {
    type: DataTypes.STRING,
    allowNull: true,
    comment: '角色描述'
  },
  menu: {
    type: DataTypes.TEXT,
    allowNull: true,
    comment: '角色所属菜单',
    defaultValue: '[]'
  },
  permission: {
    type: DataTypes.TEXT,
    allowNull: true,
    comment: '角色所属权限',
    defaultValue: '[]'
  }
})

const Permission = sequelize.define('permission', {
  // 在这里定义模型属性
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true,
    allowNull: false
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '接口名称'
  },
  path: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '接口地址'
  },
  type: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '接口类型'
  },
  basename: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '上级节点名称'
  },
  basepath: {
    type: DataTypes.STRING,
    allowNull: false,
    comment: '上级节点地址'
  },
  show: {
    type: DataTypes.INTEGER,
    allowNull: false,
    defaultValue: 1,
    comment: '是否显示'
  },
  enable: {
    type: DataTypes.INTEGER,
    defaultValue: 1,
    allowNull: false,
    comment: '是否启用'
  }
})

/**
 * 用户User 和角色Role 对应关系
 * 一个用户 -> 一个角色
 * 一个角色 -> 多个用户
 */
Role.hasMany(User, {
  foreignKey: 'rid',
  sourceKey: 'id'
})
User.belongsTo(Role, {
  foreignKey: 'rid',
  targetKey: 'id'
})

module.exports = {
  User,
  Role,
  Permission
}

routes/public.js

const Router = require('koa-router')
const router = new Router({ prefix: '/api' })
const { User } = require('../models/model')
const jwt = require('jsonwebtoken')

// 公共路由接口

// 登录获取表单数据,判断用户名和密码是否存在并正确,正确生成 token 返回给前端
router.post('/login', async (ctx, next) => {
  // 使用 postman body row 的 json 格式测试获取数据
  const body = ctx.request.body
  const user = await User.findOne({ 
    where: {
      username: body.username,
      password: body.password
    }
  })
  if (user === null) {
    ctx.status = 0
    ctx.body = {
      code: 0,
      msg: '账号不存在或密码错误'
    }
  } else {
    ctx.session.username = body.username
    ctx.status = 200
    ctx.body = {
      code: 200,
      msg: '登录成功',
      token: jwt.sign({
        data: body.username,
        exp: Math.floor(Date.now() / 1000) + 60 * 60
      },
      'jwt_secret'
      )
    }
  }
})

module.exports = router

app.js

const koa = require('koa')
const jwt = require('koa-jwt')
const JWT = require('jsonwebtoken')
const router = require('koa-router')
const requireDirectory = require('require-directory')
const bodyParser = require('koa-bodyparser')
const session = require('koa-session')
const { User,Role } = require('./models/model')
const app = new koa()

// koa-bodyparser
app.use(bodyParser())

// koa-session
app.keys = ['some secret hurr']
const CONFIG = {
  key:'koa:sess',    /*cookie key (default is koa:sess)*/
  maxAge:86400000,   /*cookie 的过期时间maxAge in ms (default is 1 days)*/
  overwrite:true,   /*是否可以overwrite (默认default true)*/
  httpOnly:true,    /*cookie 是否只有服务器端可以访问httpOnly or not (default true)*/
  signed:true,    /*默认签名*/
  rolling:false,    /*在每次请求时强行设置cookie,这将重置cookie过期时间(默认:false)*/
  renew:true,    /*当cookie快过期时请求,会重置cookie的过期时间*/
}
app.use(session(CONFIG, app))

// koa-jwt
const unlessPath = ['/api/login', '/api/register']
app.use(jwt({
  secret: 'jwt_secret', passthrough: true }).unless({
    path: unlessPath
}))

// 全局路由拦截放在其他路由之前
app.use(async (ctx, next) => {
  // 对登录、注册等路由进行放行
  console.log(ctx)
  if (unlessPath.indexOf(ctx.url) !== -1){
    await next()
  } else {
    // 判断headers 中是否存在 authorization
    if (ctx.headers && ctx.headers.authorization === undefined) {
      ctx.status = 401
      ctx.body = {
        code: 401,
        msg: '没有访问权限'
      }
    } else {
      // 若存在,验证 token 是否等于当前登录用户的用户名,等于的话,再判断此用户的角色表中的 permission 字段
      // 是否存在 ctx.url ,是的话 next(),否则未授权
      // 在else中再深入判断它是否能够访问该接口的权限就是啦{验证token,判断用户是否有权限能访问此接口路径}
      try {
        let payload = JWT.verify(ctx.headers.authorization, 'jwt_secret') // 解密, 获取payload
        if (ctx.session.username === payload.data ) {
          const user_role = await User.findOne({
            where: {
              username: ctx.session.username
            },
            include: [Role]
          })
          const res = JSON.parse(user_role.role.permission).filter(item => {
            return new RegExp(item.path, 'g').test(ctx.url) && item.type.toUpperCase() === ctx.request.method.toUpperCase()
          })
          if (res.length === 0) {
            ctx.status = 401
            ctx.body = {
              code: 401,
              msg: '没有访问权限'
            }
          } else {
            await next()
          }
        } else {
          ctx.status = 500
          ctx.body = {
            code: 500,
            msg: '未登录'
          }
        }
      } catch (err) {
        // 捕获 jwt 的异常信息
        if (err.message === 'jwt expired') {
          ctx.status = 500
          ctx.body = {
            code: 500,
            msg: 'token 过期'
          }
        } else if (err.message === 'jwt malformed') {
          ctx.status = 500
          ctx.body = {
            code: 500,
            msg: '令牌无效'
          }
        } else {
          ctx.status = 500
          ctx.body = {
            code: 500,
            msg: err.message
          }
        }
      }
    }
  }
})

// require-directory 实现路由自动注册
// 第一个参数固定是 module,第二个参数是要注册的 router 的相对路径,第三个参数是注册每个路由之前执行的业务代码
const modules = requireDirectory(module, './routes', { visit: whenLoadModule })
function whenLoadModule (obj) {
  // 判断当前对象是否是一个 Router,这种判断方式只适用于导出时没有使用大括号的方式,
  if (obj instanceof router) {
    app.use(obj.routes() , obj.allowedMethods())
  }
}

app.listen(4000, () => {
  console.log('server is running to 4000')
})

补充:crypto 密码加密模块

cryptonode.js 自带的模块,不需要安装
核心代码

注册登录要以相同的方式进行处理,这样子密码才会一致
const crypto = require('crypto') // 加密模块
let pwd = ctx.request.body.password
let md5 = crypto.createHash('md5') // md5加密
let newPwd = md5.update(pwd).digest('hex')

加密后的密码格式为

注册

router.post('/register', async (ctx, next) => {
  const body = ctx.request.body
  let pwd = body.password
  let md5 = crypto.createHash('md5') // md5加密
  let newPwd = md5.update(pwd).digest('hex')
  const data = await User.create({
    username: body.username,
    password: newPwd,
    rid: body.rid
  })
 })

登录

router.post('/login', async (ctx, next) => {
  // 使用 postman body row 的 json 格式测试获取数据
  const body = ctx.request.body
  let pwd = body.password
  let md5 = crypto.createHash('md5')
  let newPwd = md5.update(pwd).digest('hex')
  const user = await User.findOne({ 
    where: {
      username: body.username,
      password: newPwd
    }
  })
 })

风如也
202 声望11 粉丝

分享努力写前端代码的生活