初始化

  1. $ mkdir egg-example && cd egg-example
  2. $ npm init egg --type=simple
  3. $ npm i

启动项目

  1. $ npm run dev
  2. $ open http://localhost:7001

常用插件

配置env环境变量

  1. npm install dotenv --save
  2. ## {app_root}/config/config.default.js
  3. require('dotenv').config()
  4. module.exports = appInfo => {}

开启跨域 egg-cors

  1. npm i egg-cors -S
  1. exports.cors = {
  2. enable: true,
  3. package: 'egg-cors',
  4. };
  1. // 关闭csrf
  2. config.security = {
  3. csrf: {
  4. enable: false
  5. },
  6. // 跨域白名单
  7. domainWhiteList: [ 'http://localhost:4200' ],
  8. }
  9. // 开启跨域
  10. config.cors = {
  11. origin: '*',
  12. allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
  13. }

配置路由 router-plus

  1. npm i -S egg-router-plus
  2. ## plugin
  3. routerPlus: {
  4. enable: true,
  5. package: 'egg-router-plus',
  6. }

app / router.js

  1. 'use strict'
  2. module.exports = app => {
  3. require('./routes/user')(app)
  4. }

app / routes / user.js

  1. module.exports = app => {
  2. const { router, controller } = app
  3. const { create, createMore, finById, updateById } = controller.user
  4. const userRouter = router.namespace('/users')
  5. userRouter.patch('/:id', updateById)
  6. }

mysql 配置

具体配置:https://www.eggjs.org/zh-CN/tutorials/sequelize

  1. // 数据库配置
  2. config.sequelize = {
  3. dialect: 'mysql',
  4. host: process.env.MYSQL_HOST,
  5. password: process.env.MYSQL_PWD,
  6. port: process.env.MYSQL_PORT,
  7. database: process.env.MYSQL_DATABASE,
  8. timezone: "+08:00",
  9. define: {
  10. freezeTableName: true, // 关闭复数表名
  11. paranoid: true, // 生成 deleted_at 软删字段
  12. underscored: true, // 驼峰转下划线
  13. createdAt: 'created_at', // 自定义创建时间字段
  14. updatedAt: 'updated_at',
  15. deletedAt: 'deleted_at'
  16. }
  17. }

redis 配置

  1. npm i -S egg-redis
  1. // redis
  2. config.redis = {
  3. client: {
  4. port: 6379, // Redis port
  5. host: '127.0.0.1', // Redis host
  6. password: '',
  7. db: 0,
  8. },
  9. }
  1. 'use strict';
  2. const Service = require('egg').Service;
  3. class CacheService extends Service {
  4. /**
  5. * 获取列表
  6. * @param {string} key 键
  7. * @param {boolean} isChildObject 元素是否为对象
  8. * @return { array } 返回数组
  9. */
  10. async getList(key, isChildObject = false) {
  11. const { redis } = this.app
  12. let data = await redis.lrange(key, 0, -1)
  13. if (isChildObject) {
  14. data = data.map(item => {
  15. return JSON.parse(item);
  16. });
  17. }
  18. return data;
  19. }
  20. /**
  21. * 设置列表
  22. * @param {string} key 键
  23. * @param {object|string} value 值
  24. * @param {string} type 类型:push和unshift
  25. * @param {Number} expir 过期时间 单位秒
  26. * @return { Number } 返回索引
  27. */
  28. async setList(key, value, type = 'push', expir = 0) {
  29. const { redis } = this.app
  30. if (expir > 0) {
  31. await redis.expire(key, expir);
  32. }
  33. if (typeof value === 'object') {
  34. value = JSON.stringify(value);
  35. }
  36. if (type === 'push') {
  37. return await redis.rpush(key, value);
  38. }
  39. return await redis.lpush(key, value);
  40. }
  41. /**
  42. * 设置 redis 缓存
  43. * @param { String } key 键
  44. * @param {String | Object | array} value 值
  45. * @param { Number } expir 过期时间 单位秒
  46. * @return { String } 返回成功字符串OK
  47. */
  48. async set(key, value, expir = 0) {
  49. const { redis } = this.app
  50. if (expir === 0) {
  51. return await redis.set(key, JSON.stringify(value));
  52. } else {
  53. return await redis.set(key, JSON.stringify(value), 'EX', expir);
  54. }
  55. }
  56. /**
  57. * 获取 redis 缓存
  58. * @param { String } key 键
  59. * @return { String | array | Object } 返回获取的数据
  60. */
  61. async get(key) {
  62. const { redis } = this.app
  63. const result = await redis.get(key)
  64. return JSON.parse(result)
  65. }
  66. /**
  67. * redis 自增
  68. * @param { String } key 键
  69. * @param { Number } value 自增的值
  70. * @return { Number } 返回递增值
  71. */
  72. async incr(key, number = 1) {
  73. const { redis } = this.app
  74. if (number === 1) {
  75. return await redis.incr(key)
  76. } else {
  77. return await redis.incrby(key, number)
  78. }
  79. }
  80. /**
  81. * 查询长度
  82. * @param { String } key
  83. * @return { Number } 返回数据长度
  84. */
  85. async strlen(key) {
  86. const { redis } = this.app
  87. return await redis.strlen(key)
  88. }
  89. /**
  90. * 删除指定key
  91. * @param {String} key
  92. */
  93. async remove(key) {
  94. const { redis } = this.app
  95. return await redis.del(key)
  96. }
  97. /**
  98. * 清空缓存
  99. */
  100. async clear() {
  101. return await this.app.redis.flushall()
  102. }
  103. }
  104. module.exports = CacheService;

validator

安装验证插件

  1. npm i egg-valparams

plugin 配置

  1. exports.valparams = {
  2. enable : true,
  3. package: 'egg-valparams'
  4. };

config 配置

  1. exports.valparams = {
  2. locale : 'zh-cn',
  3. throwError: false
  4. };

验证

  1. class XXXController extends app.Controller {
  2. // ...
  3. async XXX() {
  4. const {ctx} = this;
  5. ctx.validate({
  6. system : {type: 'string', required: false, defValue: 'account', desc: '系统名称'},
  7. token : {type: 'string', required: true, desc: 'token 验证'},
  8. redirect: {type: 'string', required: false, desc: '登录跳转'}
  9. });
  10. // if (config.throwError === false)
  11. if(ctx.paramErrors) {
  12. // get error infos from `ctx.paramErrors`;
  13. }
  14. let params = ctx.params;
  15. let {query, body} = ctx.request;
  16. // ctx.params = validater.ret.params;
  17. // ctx.request.query = validater.ret.query;
  18. // ctx.request.body = validater.ret.body;
  19. // ...
  20. ctx.body = query;
  21. }
  22. // ...
  23. }

验证规则

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条件时,不能接收的参数
  1. const Valparams = require('path/to/Valparams[/index]');
  2. Valparams.locale('zh-cn');
  3. function list(req, res, next) {
  4. let validater = Valparams.setParams(req, {
  5. sysID : {alias:'sid',type: 'int', required: true, desc: '所属系统id'},
  6. page : {type: 'int', required: false, defValue: 1, range:{min:0}, desc: '页码'},
  7. size : {type: 'int', required: false, defValue: 30, desc: '页面大小'},
  8. offset: {type: 'int', required: false, defValue: 0, desc: '位移'}
  9. }, {
  10. choices : [{fields: ['sysID', 'page'], count: 1, force: false}],
  11. });
  12. if (validater.err && validater.err.length) {
  13. console.log(validater.err);
  14. }
  15. else {
  16. console.log(validater);
  17. //{ query: { page: 1, size: 30 },
  18. // body: {},
  19. // params: { sysID: 2 },
  20. // all: { sysID: 2, page: 1, size: 30 },
  21. // err: null }
  22. // raw: { query: { page: 1, size: 30 },
  23. // body: {},
  24. // params: { sid: 2 },
  25. // }
  26. //}
  27. //do something
  28. }
  29. }

返回支持的类型列表

  1. Valparams.vType = {
  2. ALL : 'all',
  3. STRING : 'string',
  4. ARRAY : 'array',
  5. DATE : 'date',
  6. INT : 'int',
  7. FLOAT : 'float',
  8. LETTER : 'letter',
  9. NUMBER : 'number',
  10. IP : 'ip',
  11. EMAIL : 'email',
  12. PHONE : 'phone',
  13. URL : 'url',
  14. JSON : 'json',
  15. BOOL : 'bool',
  16. NULL : 'null',
  17. RANGE : 'range',
  18. DATERANGE : 'dateRange',
  19. INTRANGE : 'intRange',
  20. FLOATRANGE : 'floatRange',
  21. NUMBERRANGE: 'numberRange'
  22. };

jwt 鉴权

用户登录

  1. // 用户登录
  2. async login() {
  3. // 1. 查询用户信息
  4. const { ctx } = this
  5. const { email } = this.ctx.request.body
  6. const user = await ctx.service.user.findOne({ email })
  7. // 2. 生成 access_token 和 refresh_token
  8. const access_token = ctx.getToken({ id: user.id, is_refresh: false })
  9. const refresh_token = ctx.getRefreshToken({ id: user.id, is_refresh: true })
  10. // 3. access_token 和 refresh_token 存入redis
  11. ctx.service.redis.set(`user_token_${user.id}`, {
  12. access_token,
  13. refresh_token
  14. })
  15. ctx.body = {
  16. access_token,
  17. refresh_token
  18. }
  19. }

access_token 鉴权

配置鉴权中间件

  1. // 全局中间件
  2. config.middleware = [
  3. 'errorHandler',
  4. 'auth' // token 鉴权
  5. ];
  6. config.auth = {
  7. // 不需要token验证的路由
  8. ignore: ['/users/login', '/users/reg', '/users/refresh_token']
  9. }
  1. const { verify } = require('jsonwebtoken')
  2. /**
  3. * 验证token
  4. * 1. 从请求头获取token
  5. * 2. 解析token && 阻止 refresh_token 解析
  6. * 3. 判断用户是否登录(redis中获取用户信息)
  7. * 4. 判断用户是否被禁用 (mysql 中查询用户信息)
  8. * 5. 用户信息挂载到ctx.state全局对象
  9. */
  10. module.exports = (options, app) => {
  11. return async (ctx, next) => {
  12. // 1. 获取token
  13. const { authorization = '' } = ctx.header
  14. const token = authorization.replace("Bearer ", '')
  15. if (!token) {
  16. ctx.throw(401, 'token 不合法')
  17. }
  18. // 2. 解析token
  19. let user = {}
  20. try {
  21. user = verify(token, 'abcdefg')
  22. // 3. 阻止 refresh_token
  23. if (user.is_refresh) {
  24. ctx.throw(401, 'token 不合法')
  25. }
  26. } catch (err) {
  27. switch (err.name) {
  28. case 'TokenExpiredError':
  29. ctx.throw(401, `token 已过期`)
  30. break
  31. default:
  32. ctx.throw(401, `token 不合法`)
  33. break
  34. }
  35. }
  36. // 4. 判断用户是否登录(从redis中查询....)
  37. let { access_token } = await ctx.service.redis.get(`user_token_${user.id}`)
  38. if (access_token !== token) {
  39. ctx.throw(401, '请登录')
  40. }
  41. // 5. 判断用户是否被禁用
  42. user = await ctx.service.user.findOne({ id: user.id })
  43. if (!user) ctx.throw(401, '用户被禁用')
  44. // 6. 用户信息挂载全局对象
  45. ctx.state.user = user
  46. return await next(options)
  47. }
  48. }

刷新token

  1. /**
  2. * 刷新token
  3. * 1. 请求头获取 refresh_token
  4. * 2. 解析 refresh_token && 阻止 access_token 解析
  5. * 3. 判断用户是否登录
  6. * 4. 判断用户是否禁用
  7. * 5. 生成新的 access_token 和 refresh_token
  8. *
  9. */
  10. const { verify } = require('jsonwebtoken')
  11. module.exports = (options, app) => {
  12. return async (ctx, next) => {
  13. // 1. 请求头获取token
  14. const { authorization = '' } = ctx.header
  15. const token = authorization.replace('Bearer ', '')
  16. if (!token) ctx.throw(401, 'token 不合法')
  17. // 2. 解析 refresh_token
  18. let user = {}
  19. try {
  20. user = verify(token, 'abcdefg')
  21. // 3. 阻止 access_token 解析
  22. if (!user.is_refresh) { ctx.throw(401, 'refresh_token 不合法') }
  23. } catch (err) {
  24. switch (err.name) {
  25. case 'TokenExpiredError':
  26. ctx.throw(403, `refresh_token 已过期`)
  27. break
  28. default:
  29. ctx.throw(401, `refresh_token 不合法`)
  30. break
  31. }
  32. }
  33. // 4. 判断用户是否登录 (根据用户id查询redis)
  34. const { refresh_token } = await ctx.service.redis.get(`user_token_${user.id}`)
  35. if (refresh_token !== token) {
  36. ctx.throw(401, '请登录')
  37. }
  38. // 5. 判断用户是否被禁用
  39. user = await ctx.service.user.findOne({ id: user.id })
  40. if (!user) {
  41. ctx.throw(401, '用户被禁用')
  42. }
  43. // 6. 生成新的 access_token 和 refresh_token
  44. const accessToken = ctx.getToken({ id: user.id, is_refresh: false })
  45. const refreshToken = ctx.getRefreshToken({ id: user.id, is_refresh: true })
  46. // 7. 更新旧的refresh_token
  47. ctx.service.redis.set(
  48. `user_token_${user.id}`,
  49. { access_token: accessToken, refresh_token: refreshToken }
  50. )
  51. // 8. 新生成的token返回客户端
  52. ctx.state.generateToken = {
  53. access_token: accessToken,
  54. refresh_token: refreshToken
  55. }
  56. await next(options)
  57. }
  58. }