Egg.js 实战(实现一个接口服务)

Egg.js 实战(实现一个接口服务)

在之前我们已经了解过 Node.jsKoa.js 的一些相关知识,今天我们来看看如何使用 Egg.js 来实现一个接口服务,Egg.js 是一个基于 Koa.js 框架而实现的框架,所以它应当属于框架之上的框架,它继承了 Koa.js 的高性能优点,同时又加入了一些『约束与开发规范』,来规避 Koa.js 框架本身的开发自由度太高的问题

Koa.js 是一个比较基层的框架,它本身没有太多约束与规范,自由度非常高,每一个开发者实现自己的服务的时候,代码风格都可以能不太一样,而 Egg.js 为了适应企业开发,加了一些开发时的规范与约束,从而解决 Koa.js 这种自由度过高而导致不适合企业内使用的缺点,Egg.js 便在这种背景下诞生,关于 Egg.js 更多的特性,这里我们只做简单介绍,更多的可以参考官网 egg.js

需求

需求比较简单,只需要实现一个接口服务即可,简单来说就是实现一个连接数据库,查询数据库里的数据并且提供一个 HTTP 接口服务,下面我们来看看如何实现

实现

首先安装 Egg.js,根据官方文档提供的方法即可

1
2
3
4
5
$ npm init egg --type=simple

$ npm i

$ npm run dev

但是这里有一个需要注意的地方,如果想要使用 npm init egg 命令,npm 的版本需要 >= 6.1.0

启动成功以后,我们先来建立一张表,用于我们的后续操作,使用的 SQL 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE `Tab_User_Info` (
id INT(100) AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL COMMENT '姓名',
uid VARCHAR(50) NOT NULL,
sex tinyint(2) DEFAULT 1 COMMENT '1男2女',
age tinyint(2) DEFAULT 1,
description VARCHAR(50) DEFAULT NULL,
`createdAt` datetime DEFAULT CURRENT_TIMESTAMP,
`updatedAt` datetime DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT = 'test user';

INSERT INTO Tab_User_Info (`name`, uid, sex, age, description) VALUES
('zhangsan', 'uid123', 1, 24, 'this is boy'),
('lisi', 'uid124', 2, 24, 'this is girl'),
('wangwu', 'uid125', 1, 26, 'this is test user'),
('zhaoliu', 'uid126', 2, 44, 'this is test user5'),
('test01', 'uid127', 2, 64, 'this is test user4'),
('test02', 'uid128', 1, 14, 'this is test user2'),
('test03', 'uid129', 2, 4, 'this is test user9');

完成后结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| test |
+--------------------+
2 rows in set (0.00 sec)

mysql> use test;
Database changed

mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| tab_user_info |
+----------------+
1 row in set (0.00 sec)

mysql> select * from tab_user_info;
+----+----------+--------+------+------+--------------------+---------------------+---------------------+
| id | name | uid | sex | age | description | createdAt | updatedAt |
+----+----------+--------+------+------+--------------------+---------------------+---------------------+
| 1 | zhangsan | uid123 | 1 | 24 | this is boy | 2019-11-12 17:51:38 | 2019-11-12 17:51:38 |
| 2 | lisi | uid124 | 2 | 24 | this is girl | 2019-11-12 17:51:38 | 2019-11-12 17:51:38 |
| 3 | wangwu | uid125 | 1 | 26 | this is test user | 2019-11-12 17:51:38 | 2019-11-12 17:51:38 |
| 4 | zhaoliu | uid126 | 2 | 44 | this is test user5 | 2019-11-12 17:51:38 | 2019-11-12 17:51:38 |
| 5 | test01 | uid127 | 2 | 64 | this is test user4 | 2019-11-12 17:51:38 | 2019-11-12 17:51:38 |
| 6 | test02 | uid128 | 1 | 14 | this is test user2 | 2019-11-12 17:51:38 | 2019-11-12 17:51:38 |
| 7 | test03 | uid129 | 2 | 4 | this is test user9 | 2019-11-12 17:51:38 | 2019-11-12 17:51:38 |
+----+----------+--------+------+------+--------------------+---------------------+---------------------+
7 rows in set (0.00 sec)

连接数据库

首先安装 mysql 插件 egg-mysql

1
$ npm install egg-mysql --save

接下来修改目录下的配置文件,开启 mysql 插件,更多的配置参数可以参考官方文档 egg-mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// app/config/plugin.js
module.exports = {
mysql: {
enable: true,
package: 'egg-mysql',
},
};


// app/config/config.default.js
const mysql = {
// 单数据库信息配置
client: {
host: '127.0.0.1',
port: '3306',
user: 'root',
password: '',
database: 'test',
},
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
};

return {
mysql
};

路由

然后再来实现路由

1
2
3
4
5
6
7
// app/router.js
module.exports = app => {
const { router, controller } = app
router.get('/', controller.home.index)
router.get('/user/list', controller.user.list)
router.get('/user/find', controller.user.find)
}

服务

然后我们来添加两个服务,一个 searchAll() 方法和一个 find(id) 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/service/user.js
'use strict'

const Service = require('egg').Service

class UserService extends Service {
async searchAll() {
const users = await this.app.mysql.select('tab_user_info')
return { users }
}

async find(id) {
const user = await this.app.mysql.get('tab_user_info', { id })
return { user }
}
}

module.exports = UserService

控制器

我们之前设定了两个服务,现在就建立一个对应的控制器来进行使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// app/controller/user.js
'use strict'

const Controller = require('egg').Controller

class UserController extends Controller {
async list() {
const { ctx } = this
try {
const userList = await ctx.service.user.searchAll()
ctx.body = {
success: true,
data: userList,
}
} catch (error) {
ctx.body = {
success: false,
error,
}
}
}

async find() {
const { ctx } = this
try {
if (!ctx.query.id) throw new Error('缺少参数')
const userList = await ctx.service.user.find(ctx.query.id)
ctx.body = {
success: true,
data: userList,
}
} catch (error) {
ctx.body = {
success: false,
error,
}
}
}
}

module.exports = UserController

验证

下面我们就可以在浏览器当中访问 http://127.0.0.1:7001/user/list 来访问我们的接口,可以发现已经将数据库当中所有的列表信息展示了出来,如果想针对单独的 id 进行查询,只需要访问 find 接口,然后传递参数即可,例如 http://127.0.0.1:7001/user/find?id=7

逻辑很简单,当路由匹配到我们对应访问的地址的时候(/user/list)就回去调用我们对应的控制器(controller.user.list),然后在控制器当中又回去访问我们之前定义的服务来进行数据库的数据查询

相关问题汇总

本节主要记录在学习 Egg.js 相关知识的时候遇到的一些坑或者知识点

query && queries

url 中的 ? 后面的部分是一个 Query String,这一部分经常用于 GET 类型的请求中传递参数,例如 GET /search?name=zhangsan&age=18name=zhangsan&age=18 就是用户传递过来的参数,Egg.js 已经帮我们封装好了获取方式,所以我们可以直接通过 this.ctx.query 来拿到解析过后的这个参数体

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict'

const Controller = require('egg').Controller

class NewsController extends Controller {
async index() {
const query = this.ctx.query
console.log(query.age) // 18
console.log(query) // { name: 'zhangsan', age: '18' }
}
}

module.exports = NewsController

不过这样的使用方式上有一点需要注意的地方

Query String 中的 key 重复时,this.ctx.query 只会取 key 第一次出现时的值,后面再出现的都会被忽略,比如 GET /search?name=zhangsan&name=lisi 通过 this.ctx.query 拿到的值是 { name: 'zhangsan' }

但是有时候用户会传递相同的 key,例如 GET /search?name=zhangsan&id=123&id=456,针对此类情况,框架提供了 this.ctx.queries 对象,这个对象也解析了 Query String,但是它不会丢弃任何一个重复的数据,而是将他们都放到一个数组中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict'

const Controller = require('egg').Controller

class NewsController extends Controller {
async index() {
// GET /search?name=zhangsan&id=123&id=456
console.log(this.ctx.queries)
// {
// name: ['zhangsan'],
// id: ['123', '456'],
// }
}
}

module.exports = NewsController

this.ctx.queries 上所有的 key 如果有值,也一定会是数组类型

helper

helper 函数用来提供一些实用的工具函数,它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,下面来看一个比较常见的实例,序列化模版引擎当中的日期格式,我们先在 helper.js 当中定义我们格式化的方法

这里有个需要注意的地方,定义的文件名字需要是一致的,因为框架会把 app/extend/helper.js 中定义的对象与内置 helperprototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 helper 对象,这里使用的是 silly-datetime 这个日期库,使用其他的也是可行的

1
2
3
4
5
6
7
8
9
10
// app/extend/helper.js
'use strict'

const sd = require('silly-datetime')

module.exports = {
formatTime(time) {
return sd.format(new Date(time * 1000), 'YYYY-MM-DD HH:mm')
},
}

然后在模版当中不需要引入之类的操作,直接使用即可(helper.formatTime()

1
2
3
4
5
6
7
8
<!-- app/view/news.html -->
<ul>
<% for (var i = 0; i < list.length; i++) {%>
<li>
<a href="/newscontent?aid=<%= list[i].aid %>"><%= list[i].title %></a> --- <span><%= helper.formatTime(list[i].datetime) %></span>
</li>
<% } %>
</ul>

跨域请求设置

可以使用 egg-cors 这个库,先配置 plugin.js

1
2
3
4
5
// config/plugin.js
exports.cors = {
enable: true,
package: 'egg-cors',
}

然后在配置 config.default.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// config/config.default.js
config.security = {
csrf: {
enable: false,
ignoreJSON: true,
},
// 配置白名单
domainWhiteList: ['http://www.baidu.com'],
}

config.cors = {
// 允许所有跨域访问,如果注释掉则允许上面 白名单 访问
// origin: '*',
allowMethods: 'GET, HEAD, PUT, POST, DELETE, PATCH',
}

不要使用全部允许跨域,可能会引起安全方面的问题,建议配置访问白名单(注释掉 origin 即可)

使用 koa 的中间件

在框架里面可以非常容易的引入 Koa 中间件生态,以 koa-compress 为例,在 Koa 中使用时

1
2
3
4
5
6
7
const koa = require('koa')
const compress = require('koa-compress')

const app = koa()

const options = { threshold: 2048 }
app.use(compress(options))

Egg.js 当中使用如下

1
2
// app/middleware/compress.js
module.exports = require('koa-compress')

一个需要注意的地方,koa-compress 暴露的接口((options) => middleware)和框架对中间件要求一致,配置中间件

1
2
3
4
5
6
7
// config/config.default.js
module.exports = {
middleware: ['compress'],
compress: {
threshold: 2048,
},
}

表单内容的获取

先来看如下代码

1
2
3
4
5
6
7
8
9
// app/router.js
module.exports = app => {
app.router.post('/form', app.controller.form.post)
}

// app/controller/form.js
exports.post = async ctx => {
ctx.body = `body: ${JSON.stringify(ctx.request.body)}`
}

这里如果直接发起 POST 请求是会报错的,错误提示为 missing csrf token,简单来说,因为框架中内置了安全插件 egg-security,提供了一些默认的安全实践,并且框架的安全插件是默认开启的,如果需要关闭其中一些安全防范,直接设置该项的 enable 属性为 false 即可

1
2
3
exports.security = {
csrf: false
}

路由重定向

内部重定向

1
2
3
4
5
6
7
8
9
10
11
// app/router.js
module.exports = app => {
app.router.get('index', '/home/index', app.controller.home.index)
// 访问根目录自动重定向到 /home/index
app.router.redirect('/', '/home/index', 303)
}

// app/controller/home.js
exports.index = async ctx => {
ctx.body = 'hello controller'
}

外部重定向

1
2
3
4
5
6
7
8
9
10
exports.index = async ctx => {
const type = ctx.query.type
const q = ctx.query.q || 'nodejs'

if (type === 'bing') {
ctx.redirect(`http://cn.bing.com/search?q=${q}`)
} else {
ctx.redirect(`https://www.google.co.kr/search?q=${q}`)
}
}

自定义控制器基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/core/base_controller.js
const { Controller } = require('egg')

class BaseController extends Controller {
get user() {
return this.ctx.session.user
}

success(data) {
this.ctx.body = {
success: true,
data,
}
}

notFound(msg) {
msg = msg || 'not found'
this.ctx.throw(404, msg)
}
}

module.exports = BaseController

此时在编写应用的 Controller 时,可以继承 BaseController,直接使用基类上的方法

1
2
3
4
5
6
7
8
9
10
11
//app/controller/post.js
const Controller = require('../core/base_controller')

class PostController extends Controller {
async list() {
const posts = await this.service.listByUser(this.user)

// 使用基类的方法
this.success(posts)
}
}

文件上传

一般来说,浏览器上都是通过 Multipart/form-data 格式发送文件的,框架通过内置 Multipart 插件来支持获取用户上传的文件,首先需要在 config 文件中启用 file 模式

1
2
3
4
// config/config.default.js
exports.multipart = {
mode: 'file',
}

然后就可以进行使用了,这里主要分为两种情况,上传单个文件和上传多个文件,我们先来看单文件情况

1
2
3
4
5
<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
title: <input name="title" />
file: <input name="file" type="file" />
<button type="submit">Upload</button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// app/controller/upload.js
const Controller = require('egg').Controller
const fs = require('mz/fs')

module.exports = class extends Controller {
async upload() {
const { ctx } = this
const file = ctx.request.files[0]
const name = 'egg-multipart-test/' + path.basename(file.filename)
let result
try {
// 处理文件,比如上传到云端
result = await ctx.oss.put(name, file.filepath)
} finally {
// 需要删除临时文件
await fs.unlink(file.filepath)
}

ctx.body = {
url: result.url,
// 获取所有的字段值
requestBody: ctx.request.body,
}
}
}

对于多个文件,我们借助 ctx.request.files 属性进行遍历,然后分别进行处理,HTML 还是一样的,不过添加了可接受多个值的文件上传字段 multiple,这里主要来看后端是如何处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// app/controller/upload.js
const Controller = require('egg').Controller
const fs = require('mz/fs')

module.exports = class extends Controller {
async upload() {
const { ctx } = this
console.log(ctx.request.body)
console.log('got %d files', ctx.request.files.length)

for (const file of ctx.request.files) {

console.log('field: ' + file.fieldname)
console.log('filename: ' + file.filename)
console.log('encoding: ' + file.encoding)
console.log('mime: ' + file.mime)
console.log('tmp filepath: ' + file.filepath)

let result
try {
// 处理文件,比如上传到云端
result = await ctx.oss.put('egg-multipart-test/' + file.filename, file.filepath)
} finally {
// 需要删除临时文件
await fs.unlink(file.filepath)
}
console.log(result)
}
}
}

但是这里有个需要注意的地方,为了保证文件上传的安全,框架限制了支持的的文件格式,框架默认支持白名单如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const whitelist = [
// images
'.jpg', '.jpeg', // image/jpeg
'.png', // image/png, image/x-png
'.gif', // image/gif
'.bmp', // image/bmp
'.wbmp', // image/vnd.wap.wbmp
'.webp',
'.tif',
'.psd',
// text
'.svg',
'.js', '.jsx',
'.json',
'.css', '.less',
'.html', '.htm',
'.xml',
// tar
'.zip',
'.gz', '.tgz', '.gzip',
// video
'.mp3',
'.mp4',
'.avi',
]

但是我们可以通过在 config/config.default.js 中配置来新增支持的文件扩展名

1
2
3
4
5
6
7
// 新增支持的文件扩展名
module.exports = {
multipart: {
// 增加对 apk 扩展名的文件支持
fileExtensions: ['.apk']
},
}

或者重写整个白名单

1
2
3
4
5
6
// 覆盖整个白名单,只允许上传 '.png' 格式
module.exports = {
multipart: {
whitelist: ['.png'],
},
}

更多详细可以参考文档 egg-multipart

服务(service)

注意事项,service 文件必须放在 app/service 目录,可以支持多级目录,访问的时候可以通过目录名级联访问

1
2
3
app/service/biz/user.js    ==>  ctx.service.biz.user     // 多级目录,依据目录名级联访问
app/service/sync_user.js ==> ctx.service.syncUser // 下划线自动转换为自动驼峰
app/service/HackerNews.js ==> ctx.service.hackerNews // 大写自动转换为驼峰
  • 一个 service 文件只能包含一个类,这个类需要通过 module.exports 的方式返回
  • service 需要通过 class 的方式定义,父类必须是 egg.service
  • service 不是单例,是『请求级别』的对象,框架在每次请求中首次访问 ctx.service.xx 时延迟实例化,所以 service 中可以通过 this.ctx 获取到当前请求的上下文

下面是一个实际的使用例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// app/controller/user.js
const Controller = require('egg').Controller

class UserController extends Controller {
async info() {
const userId = this.ctx.params.id
const userInfo = await this.ctx.service.user.find(userId)
this.ctx.body = userInfo
}
}

module.exports = UserController


// app/service/user.js
const Service = require('egg').Service

class UserService extends Service {
// 默认不需要提供构造函数,如果需要在构造函数做一些处理,需要调用 super(ctx) 才能保证后面 this.ctx 的使用
// 调用之后就可以直接通过 this.ctx 获取 ctx 和通过 this.app 获取 app 了
// constructor(ctx) {
// super(ctx)
// }
async find(uid) {
// 假如 我们拿到用户 id 从数据库获取用户详细信息
const user = await this.ctx.db.query('select * from user where uid = ?', uid)

// 假定这里还有一些复杂的计算,然后返回需要的信息。
const picture = await this.getPicture(uid)

return {
name: user.user_name,
age: user.age,
picture,
}
}

async getPicture(uid) {
const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, { dataType: 'json' })
return result.data
}
}

module.exports = UserService

属性扩展

一般来说属性的计算只需要进行一次,那么一定要实现缓存,否则在多次访问属性时会计算多次,这样会降低应用性能,推荐的方式是使用 Symbol + Getter 的模式

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/extend/application.js
const BAR = Symbol('Application#bar')

module.exports = {
get bar() {
// this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性
if (!this[BAR]) {
// 实际情况肯定更复杂
this[BAR] = this.config.xx + this.config.yy
}
return this[BAR]
},
}

session 存储

框架内置了 egg-session 插件,给我们提供了 ctx.session 来访问或者修改当前用户 Session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class HomeController extends Controller {
async fetchPosts() {
const ctx = this.ctx
// 获取 Session 上的内容
const userId = ctx.session.userId
const posts = await ctx.service.post.fetch(userId)
// 修改 Session 的值
ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1
ctx.body = {
success: true,
posts,
}
}
}

但是有一个特别需要注意的地方,在设置 Session 属性时需要避免以下几种情况,因为会造成字段丢失(koa-session

  • 不要以 _ 开头
  • 不能为 isNew
1
2
3
4
5
6
// ❌ 错误的用法
ctx.session._visited = 1 // ==> 该字段会在下一次请求时丢失
ctx.session.isNew = 'lisi' // ==> 为内部关键字, 不应该去更改

// ✔️ 正确的用法
ctx.session.visited = 1 // ==> 此处没有问题

Session 默认存放在 Cookie 中,但是如果我们的 Session 对象过于庞大,就会带来一些额外的问题

  • 浏览器通常都有限制最大的 Cookie 长度,当设置的 Session 过大时,浏览器可能拒绝保存
  • Cookie 在每次请求时都会带上,当 Session 过大时,每次请求都要额外带上庞大的 Cookie 信息

我们只需要设置 app.sessionStore 即可将 Session 存储到指定的存储中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app.js
module.exports = app => {
app.sessionStore = {
// support promise/async
async get(key) {
// return value
},
async set(key, value, maxAge) {
// set key to store
},
async destroy(key) {
// destroy key
},
}
}

sessionStore 的实现我们也可以封装到插件中,例如 egg-session-redis 就提供了将 Session 存储到 redis 中的能力,在应用层我们只需要引入 egg-redisegg-session-redis 插件即可

1
2
3
4
5
6
7
8
9
10
// plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
}

exports.sessionRedis = {
enable: true,
package: 'egg-session-redis',
}

但是需要注意的是,一旦选择了将 Session 存入到外部存储中,就意味着系统将强依赖于这个外部存储,当它挂了的时候,就完全无法使用 Session 相关的功能了,因此更推荐只将必要的信息存储在 Session 中,保持 Session 的精简并使用默认的 Cookie 存储,用户级别的缓存不要存储在 Session

egg-jwt

本章节主要介绍使用 egg 验证 Token 的过程,首先初始化一个项目,然后安装两个用于我们之后操作所使用的包

1
2
3
4
5
6
// 使用 ts 版本
npm init egg --type=ts
npm install

// 安装跨域包以及 token 的生成以及验证包
npm install egg-cors egg-jwt --save

安装完成后首先来配置 config/plugin.ts 当中的两个验证包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { EggPlugin } from 'egg'

const plugin: EggPlugin = {
jwt: {
enable: true,
package: "egg-jwt"
},
cors: {
enable: true,
package: 'egg-cors',
}
}

export default plugin

接下来是 config/config.default.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
config.jwt = {
//自定义 token 的加密条件字符串
secret: 'abc'
}

config.security = {
csrf: {
enable: false,
ignoreJSON: true
},
// 允许访问接口的白名单
domainWhiteList: ['http://localhost:8080'],
}

config.cors = {
origin: '*',
allowMethods: 'GET, HEAD, PUT, POST, DELETE, PATCH'
}

最后一步操作,也是 TypeScript 独有的坑,需要在根目录下的 typings/index.d.ts 文件里声明一个 any 类型,否则会类型错误

1
2
3
4
5
6
7
import 'egg'

declare module 'egg' {
interface Application {
jwt: any
}
}

下面就是具体操作流程,首先来定义路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/router.ts
import { Application } from 'egg'

export default (app: Application) => {
const { controller, router, jwt } = app

// 正常路由
router.post('/admin/login', controller.admin.login)

/*
* 这里的第二个对象不再是控制器,而是 jwt 验证对象,第三个地方才是控制器
* 只有在需要验证 token 的路由才需要第二个 是 jwt 否则第二个对象为控制器
『/
router.post('/admin', jwt, controller.admin.index)
}

接下来就是设定控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { Controller } from 'egg'

export default class AdminController extends Controller {

// 验证登录并且生成 token
public async login() {
const { ctx, app } = this

// 获取用户端传递过来的参数
const data = ctx.request.body

// 进行验证 data 数据 登录是否成功

// ...

// 成功过后进行一下操作

// 生成 token 的方式
const token = app.jwt.sign({

// 需要存储的 token 数据
username: data.username,

// ...

}, app.config.jwt.secret)

// 生成的token = eyJhbGciOiJIUzI1...

// 返回 token 到前端
ctx.body = token
}

// 访问 admin 数据时进行验证 token,并且解析 token 的数据
public async index() {

const { ctx, app } = this

console.log(ctx.state.user)
/*
* 打印内容为:{ username : 'admin', iat: 1560346903 }
* iat 为过期时间,可以单独写中间件验证,这里不做细究
* 除了 iat 之后,其余的为当时存储的数据
『/

ctx.body = { code: 0, msg: '验证成功' }
}
}

最后前端在请求的时候需要在 headers 里面上添加上默认的验证字断 Authorization 就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
axios({
method: 'post',
url: 'http://127.0.0.1:7001/admin',
data: {
username: 'admin',
lastName: '123456'
},
headers: {
// 切记 token 不要直接发送,要在前面加上 Bearer 字符串和一个空格
'Authorization': `Bearer ${token}`
}
}).then(res => {
console.log(res.data)
})

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×