项目环境: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.使用
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 密码加密模块
crypto
是 node.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
}
})
})