koa中基于JWT的用户权限管理详细说明

项目环境: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 密码
rid role 表外键

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.使用

    项目目录结构
    koa中基于JWT的用户权限管理详细说明_第1张图片

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

你可能感兴趣的:(后端前端)