初始化
$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i
启动项目
$ npm run dev
$ open http://localhost:7001
常用插件
配置env环境变量
npm install dotenv --save
## {app_root}/config/config.default.js
require('dotenv').config()
module.exports = appInfo => {}
开启跨域 egg-cors
npm i egg-cors -S
exports.cors = {
enable: true,
package: 'egg-cors',
};
// 关闭csrf
config.security = {
csrf: {
enable: false
},
// 跨域白名单
domainWhiteList: [ 'http://localhost:4200' ],
}
// 开启跨域
config.cors = {
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
}
配置路由 router-plus
npm i -S egg-router-plus
## plugin
routerPlus: {
enable: true,
package: 'egg-router-plus',
}
app / router.js
'use strict'
module.exports = app => {
require('./routes/user')(app)
}
app / routes / user.js
module.exports = app => {
const { router, controller } = app
const { create, createMore, finById, updateById } = controller.user
const userRouter = router.namespace('/users')
userRouter.patch('/:id', updateById)
}
mysql 配置
具体配置:https://www.eggjs.org/zh-CN/tutorials/sequelize
// 数据库配置
config.sequelize = {
dialect: 'mysql',
host: process.env.MYSQL_HOST,
password: process.env.MYSQL_PWD,
port: process.env.MYSQL_PORT,
database: process.env.MYSQL_DATABASE,
timezone: "+08:00",
define: {
freezeTableName: true, // 关闭复数表名
paranoid: true, // 生成 deleted_at 软删字段
underscored: true, // 驼峰转下划线
createdAt: 'created_at', // 自定义创建时间字段
updatedAt: 'updated_at',
deletedAt: 'deleted_at'
}
}
redis 配置
npm i -S egg-redis
// redis
config.redis = {
client: {
port: 6379, // Redis port
host: '127.0.0.1', // Redis host
password: '',
db: 0,
},
}
'use strict';
const Service = require('egg').Service;
class CacheService extends Service {
/**
* 获取列表
* @param {string} key 键
* @param {boolean} isChildObject 元素是否为对象
* @return { array } 返回数组
*/
async getList(key, isChildObject = false) {
const { redis } = this.app
let data = await redis.lrange(key, 0, -1)
if (isChildObject) {
data = data.map(item => {
return JSON.parse(item);
});
}
return data;
}
/**
* 设置列表
* @param {string} key 键
* @param {object|string} value 值
* @param {string} type 类型:push和unshift
* @param {Number} expir 过期时间 单位秒
* @return { Number } 返回索引
*/
async setList(key, value, type = 'push', expir = 0) {
const { redis } = this.app
if (expir > 0) {
await redis.expire(key, expir);
}
if (typeof value === 'object') {
value = JSON.stringify(value);
}
if (type === 'push') {
return await redis.rpush(key, value);
}
return await redis.lpush(key, value);
}
/**
* 设置 redis 缓存
* @param { String } key 键
* @param {String | Object | array} value 值
* @param { Number } expir 过期时间 单位秒
* @return { String } 返回成功字符串OK
*/
async set(key, value, expir = 0) {
const { redis } = this.app
if (expir === 0) {
return await redis.set(key, JSON.stringify(value));
} else {
return await redis.set(key, JSON.stringify(value), 'EX', expir);
}
}
/**
* 获取 redis 缓存
* @param { String } key 键
* @return { String | array | Object } 返回获取的数据
*/
async get(key) {
const { redis } = this.app
const result = await redis.get(key)
return JSON.parse(result)
}
/**
* redis 自增
* @param { String } key 键
* @param { Number } value 自增的值
* @return { Number } 返回递增值
*/
async incr(key, number = 1) {
const { redis } = this.app
if (number === 1) {
return await redis.incr(key)
} else {
return await redis.incrby(key, number)
}
}
/**
* 查询长度
* @param { String } key
* @return { Number } 返回数据长度
*/
async strlen(key) {
const { redis } = this.app
return await redis.strlen(key)
}
/**
* 删除指定key
* @param {String} key
*/
async remove(key) {
const { redis } = this.app
return await redis.del(key)
}
/**
* 清空缓存
*/
async clear() {
return await this.app.redis.flushall()
}
}
module.exports = CacheService;
validator
安装验证插件
npm i egg-valparams
plugin 配置
exports.valparams = {
enable : true,
package: 'egg-valparams'
};
config 配置
exports.valparams = {
locale : 'zh-cn',
throwError: false
};
验证
class XXXController extends app.Controller {
// ...
async XXX() {
const {ctx} = this;
ctx.validate({
system : {type: 'string', required: false, defValue: 'account', desc: '系统名称'},
token : {type: 'string', required: true, desc: 'token 验证'},
redirect: {type: 'string', required: false, desc: '登录跳转'}
});
// if (config.throwError === false)
if(ctx.paramErrors) {
// get error infos from `ctx.paramErrors`;
}
let params = ctx.params;
let {query, body} = ctx.request;
// ctx.params = validater.ret.params;
// ctx.request.query = validater.ret.query;
// ctx.request.body = validater.ret.body;
// ...
ctx.body = query;
}
// ...
}
验证规则
Valparams.setParams(req, params, options);
Param | Type | Description | Example |
---|---|---|---|
req | Object | request 对象,这里我们就是取相应的三种请求的参数进行参数验证 | {params, query, body} |
params | Object | 参数的格式配置 { pname: {alias, type, required, range: {in, min, max, reg, schema }, defValue, trim, allowEmptyStr, desc[, detail] } } | {sysID : {alias:’sid’,type: ‘int’, required: true, desc: ‘所属系统id’}} |
params[pname] | String | 参数名 | |
params[pname].alias | String | 参数别名,可以使用该参数指定前端使用的参数名称 | |
params[pname].type | String | 参数类型 | 常用可选类型有 int, string, json 等,其他具体可见下文或用 Valparams.vType 进行查询 |
params[pname].required | Boolean | 是否必须 | |
params[pname].range | Object | 参数范围控制 | {min: ‘112.80.248.10’, max: ‘112.80.248.72’} |
params[pname].range.min | ALL | 最小值、最短、最早(不同 type 参数 含义有所差异) | |
params[pname].range.max | ALL | 最大值、最长、最晚(不同 type 参数 含义有所差异) | |
params[pname].range.in | Array | 在XX中,指定参数必须为其中的值 | |
params[pname].range.reg | RegExp | 正则判断,参数需要符合正则 | |
params[pname].range.schema | Object | jsonSchema,针对JSON类型参数有效,使用ajv对参数进行格式控制 | |
params[pname].defValue | ALL | 默认值,没传参数或参数验证出错时生效,此时会将该值赋值到相应参数上 | |
params[pname].trim | Boolean | 是否去掉参数前后空格字符,默认false | |
params[pname].allowEmptyStr | Boolean | 是否允许接受空字符串,默认false | |
params[pname].desc | String | 参数含义描述 | |
options | Object | 参数关系配置 | |
options.choices | Array | 参数挑选规则 | [{fields: [‘p22’, ‘p23’, ‘p24’], count: 2, force: true}] 表示’p22’, ‘p23’, ‘p24’ 参数三选二 |
options.choices[].fields | Array | 涉及的参数 | |
options.choices[].count | Number | 需要至少传 ${count} 个 | |
options.choices[].force | Boolean | 默认 false,为 true 时,涉及的参数中只能传 ${count} 个, 为 false 时,可以多于 ${count} 个 | |
options.equals | Array | 参数相等 | [[‘p20’, ‘p21’], [‘p22’, ‘p23’]] 表示 ‘p20’, ‘p21’ 两个值需要相等,’p22’, ‘p23’ 两个值需要相等 |
options.equals[] | Array | 涉及的参数(涉及的参数的值需要是相等的) | |
options.compares | Array | 参数大小关系 | [[‘p25’, ‘p26’, ‘p27’]] 表示 ‘p25’, ‘p26’, ‘p27’ 必须符合 ‘p25’ <= ‘p26’ <= ‘p27’ |
options.compares[] | Array | 涉及的参数(涉及的参数的值需要是按顺序从小到大的) | |
options.cases | Object | 参数条件判断 | [{when: [‘p30’], then: [‘p31’], not: [‘p32’]}] 表示 当传了 p30 就必须传 p31 ,同时不能传p32 |
options.cases.when | Array | 条件 | |
options.cases.when[] | String | 涉及的参数,(字符串)只要接收到的参数有这个字段即为真 | |
options.cases.when[].field | 涉及的参数的名(对象) | —- | |
options.cases.when[].value | 涉及的参数的值(对象)需要参数的值与该值相等才为真 | —- | |
options.cases.then | Array | 符合when条件时,需要必传的参数 | |
options.cases.not | Array | 符合when条件时,不能接收的参数 |
const Valparams = require('path/to/Valparams[/index]');
Valparams.locale('zh-cn');
function list(req, res, next) {
let validater = Valparams.setParams(req, {
sysID : {alias:'sid',type: 'int', required: true, desc: '所属系统id'},
page : {type: 'int', required: false, defValue: 1, range:{min:0}, desc: '页码'},
size : {type: 'int', required: false, defValue: 30, desc: '页面大小'},
offset: {type: 'int', required: false, defValue: 0, desc: '位移'}
}, {
choices : [{fields: ['sysID', 'page'], count: 1, force: false}],
});
if (validater.err && validater.err.length) {
console.log(validater.err);
}
else {
console.log(validater);
//{ query: { page: 1, size: 30 },
// body: {},
// params: { sysID: 2 },
// all: { sysID: 2, page: 1, size: 30 },
// err: null }
// raw: { query: { page: 1, size: 30 },
// body: {},
// params: { sid: 2 },
// }
//}
//do something
}
}
返回支持的类型列表
Valparams.vType = {
ALL : 'all',
STRING : 'string',
ARRAY : 'array',
DATE : 'date',
INT : 'int',
FLOAT : 'float',
LETTER : 'letter',
NUMBER : 'number',
IP : 'ip',
EMAIL : 'email',
PHONE : 'phone',
URL : 'url',
JSON : 'json',
BOOL : 'bool',
NULL : 'null',
RANGE : 'range',
DATERANGE : 'dateRange',
INTRANGE : 'intRange',
FLOATRANGE : 'floatRange',
NUMBERRANGE: 'numberRange'
};
jwt 鉴权
用户登录
// 用户登录
async login() {
// 1. 查询用户信息
const { ctx } = this
const { email } = this.ctx.request.body
const user = await ctx.service.user.findOne({ email })
// 2. 生成 access_token 和 refresh_token
const access_token = ctx.getToken({ id: user.id, is_refresh: false })
const refresh_token = ctx.getRefreshToken({ id: user.id, is_refresh: true })
// 3. access_token 和 refresh_token 存入redis
ctx.service.redis.set(`user_token_${user.id}`, {
access_token,
refresh_token
})
ctx.body = {
access_token,
refresh_token
}
}
access_token 鉴权
配置鉴权中间件
// 全局中间件
config.middleware = [
'errorHandler',
'auth' // token 鉴权
];
config.auth = {
// 不需要token验证的路由
ignore: ['/users/login', '/users/reg', '/users/refresh_token']
}
const { verify } = require('jsonwebtoken')
/**
* 验证token
* 1. 从请求头获取token
* 2. 解析token && 阻止 refresh_token 解析
* 3. 判断用户是否登录(redis中获取用户信息)
* 4. 判断用户是否被禁用 (mysql 中查询用户信息)
* 5. 用户信息挂载到ctx.state全局对象
*/
module.exports = (options, app) => {
return async (ctx, next) => {
// 1. 获取token
const { authorization = '' } = ctx.header
const token = authorization.replace("Bearer ", '')
if (!token) {
ctx.throw(401, 'token 不合法')
}
// 2. 解析token
let user = {}
try {
user = verify(token, 'abcdefg')
// 3. 阻止 refresh_token
if (user.is_refresh) {
ctx.throw(401, 'token 不合法')
}
} catch (err) {
switch (err.name) {
case 'TokenExpiredError':
ctx.throw(401, `token 已过期`)
break
default:
ctx.throw(401, `token 不合法`)
break
}
}
// 4. 判断用户是否登录(从redis中查询....)
let { access_token } = await ctx.service.redis.get(`user_token_${user.id}`)
if (access_token !== token) {
ctx.throw(401, '请登录')
}
// 5. 判断用户是否被禁用
user = await ctx.service.user.findOne({ id: user.id })
if (!user) ctx.throw(401, '用户被禁用')
// 6. 用户信息挂载全局对象
ctx.state.user = user
return await next(options)
}
}
刷新token
/**
* 刷新token
* 1. 请求头获取 refresh_token
* 2. 解析 refresh_token && 阻止 access_token 解析
* 3. 判断用户是否登录
* 4. 判断用户是否禁用
* 5. 生成新的 access_token 和 refresh_token
*
*/
const { verify } = require('jsonwebtoken')
module.exports = (options, app) => {
return async (ctx, next) => {
// 1. 请求头获取token
const { authorization = '' } = ctx.header
const token = authorization.replace('Bearer ', '')
if (!token) ctx.throw(401, 'token 不合法')
// 2. 解析 refresh_token
let user = {}
try {
user = verify(token, 'abcdefg')
// 3. 阻止 access_token 解析
if (!user.is_refresh) { ctx.throw(401, 'refresh_token 不合法') }
} catch (err) {
switch (err.name) {
case 'TokenExpiredError':
ctx.throw(403, `refresh_token 已过期`)
break
default:
ctx.throw(401, `refresh_token 不合法`)
break
}
}
// 4. 判断用户是否登录 (根据用户id查询redis)
const { refresh_token } = await ctx.service.redis.get(`user_token_${user.id}`)
if (refresh_token !== token) {
ctx.throw(401, '请登录')
}
// 5. 判断用户是否被禁用
user = await ctx.service.user.findOne({ id: user.id })
if (!user) {
ctx.throw(401, '用户被禁用')
}
// 6. 生成新的 access_token 和 refresh_token
const accessToken = ctx.getToken({ id: user.id, is_refresh: false })
const refreshToken = ctx.getRefreshToken({ id: user.id, is_refresh: true })
// 7. 更新旧的refresh_token
ctx.service.redis.set(
`user_token_${user.id}`,
{ access_token: accessToken, refresh_token: refreshToken }
)
// 8. 新生成的token返回客户端
ctx.state.generateToken = {
access_token: accessToken,
refresh_token: refreshToken
}
await next(options)
}
}