一、egg-jwt实现用户鉴权

用户鉴权,一种用于在通信网络中对试图访问来自服务提供商的服务的用户进行鉴权的方法。用于用户登陆到DSMP或使用数据业务时,业务网关或Portal发送此消息到DSMP,对该用户使用数据业务的合法性和有效性(状态是否为激活)进行检查。

简单理解,鉴权就是用户在浏览网页或 App 时,通过约定好的方式,让网页和用户建立起一种相互信赖的机制,继而返回给用户需要的信息。

鉴权的机制:

  • HTTP Basic Authentication
  • session-cookie
  • Token 令牌
  • OAuth(开放授权)

token 可以运用在如网页、客户端、小程序、浏览器插件等等领域。如果选用 cookie 的形式鉴权,在客户端和小程序就无法使用这套接口,因为它们没有域的概念,而 cookie 是需要存在某个域下。

二、注册接口

controller 目录下新建 user.js 用于编写用户相关的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// controller/user.js
'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller {
async register() {
const { ctx } = this;
const { username, password } = ctx.request.body; // 获取注册需要的参数
}
}

module.exports = UserController;

此时我们拿到了 usernamepassword,我们需要判断两个参数是否为空。如果是空,则返回错误信息:

1
2
3
4
5
6
7
8
9
// 判空操作
if (!username || !password) {
ctx.body = {
code: 500,
msg: '账号密码不能为空',
data: null
}
return
}

此时我们还需要一个判断,根据用户传入的 username 去数据库的 user 表查询,是否已经被注册。

service 目录下新建 user.js,并且添加 getUserByName 方法用于根据 username 查找用户信息

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

const Service = require('egg').Service;

class UserService extends Service {
// 通过用户名获取用户信息
async getUserByName(username) {
const { app } = this;
try {
const result = await app.mysql.get('user', { username });
return result;
} catch (error) {
console.log(error);
return null;
}
}
}
module.exports = UserService;

使用 async 和 await 时,如果想捕获错误,需要使用 try…catch 来捕获,如果代码运行过程中发生错误,都将会被 catch 捕获。

controller/user.js 继续添加逻辑,在 「判空操作」逻辑下,判断是否已经被注册的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// controller/user.js
async register() {
...
// 验证数据库内是否已经有该账户名
const userInfo = await ctx.service.user.getUserByName(username) // 获取用户信息

// 判断是否已经存在
if (userInfo && userInfo.id) {
ctx.body = {
code: 500,
msg: '账户名已被注册,请重新输入',
data: null
}
return
}
}

经过上述两层判断之后,接下便可将账号和密码写入数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// controller/user.js
// 默认头像,放在 user.js 的最外,部避免重复声明。
const defaultAvatar = 'http://s.yezgea02.com/1615973940679/WeChat77d6d2ac093e247c361f0b8a7aeb6c2a.png'
// 调用 service 方法,将数据存入数据库。
const result = await ctx.service.user.register({
username,
password,
signature: '世界和平。',
avatar: defaultAvatar
});

if (result) {
ctx.body = {
code: 200,
msg: '注册成功',
data: null
}
} else {
ctx.body = {
code: 500,
msg: '注册失败',
data: null
}
}

service/user.js 添加 register 写入数据库的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// service/user.js
...
// 注册
async register(params) {
const { app } = this;
try {
const result = await app.mysql.insert('user', params);
return result;
} catch (error) {
console.log(error);
return null;
}
}

router.js 将接口抛出

1
2
3
4
5
6
7
8
9
10
// router.js
'use strict';

/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.post('/api/user/register', controller.user.register);
};

通过postman工具测试接口。

三、登录接口

通过注册的「用户名」和「密码」,调用登录接口,接口会返回给我们一个 token 令牌

每次发起请求,无论是获取数据,还是提交数据,我们都需要将 token 带上,以此来标识,此次获取(GET)或提交(POST)是哪一个用户的行为。

egg-jwt 有加密的功能,也有解密的功能。通过解密 token 拿到当初加密 token 时的信息,信息的内容大致就是当初注册时候的用户信息。

安装egg-jwt插件

1
npm i egg-jwt -S

Egg-jwt的仓库地址

config/plugin.js 下添加插件:

1
2
3
4
jwt: {
enable: true,
package: 'egg-jwt'
}

config/config.default.js 下添加自定义加密字符串

1
2
3
config.jwt = {
secret: 'YQ',
};

secret加密字符串,将在后续用于结合用户信息生成一串token

/controller/user.js 下新建 login 方法

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
async login() {
// app 为全局属性,相当于所有的插件方法都植入到了 app 对象。
const { ctx, app } = this;
const { username, password } = ctx.request.body
// 根据用户名,在数据库查找相对应的id操作
const userInfo = await ctx.service.user.getUserByName(username)
// 没找到说明没有该用户
if (!userInfo || !userInfo.id) {
ctx.body = {
code: 500,
msg: '账号不存在',
data: null
}
return
}
// 找到用户,并且判断输入密码与数据库中用户密码。
if (userInfo && password != userInfo.password) {
ctx.body = {
code: 500,
msg: '账号密码错误',
data: null
}
return
}
// 生成 token 加盐
// app.jwt.sign 方法接受两个参数,第一个为对象,对象内是需要加密的内容;第二个是加密字符串,上文已经提到过。
const token = app.jwt.sign({
id: userInfo.id,
username: userInfo.username,
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) // token 有效期为 24 小时
}, app.config.jwt.secret);

ctx.body = {
code: 200,
message: '登录成功',
data: {
token
},
};
}

把获取到的 userInfo 中的 idusername 两个属性,通过 app.jwt.sign 方法,结合 app.config.jwt.secret 加密字符串(之前声明的 YQ),生成一个 token。这个 token 会是一串很长的加密字符串

/controller/user.js 中,新增一个验证方法 test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 验证方法
async test() {
const { ctx, app } = this;
// 通过 token 解析,拿到 user_id
const token = ctx.request.header.authorization; // 请求头获取 authorization 属性,值为 token
// 通过 app.jwt.verify + 加密字符串 解析出 token 的值
const decode = await app.jwt.verify(token, app.config.jwt.secret);
// 响应接口
ctx.body = {
code: 200,
message: '获取成功',
data: {
...decode
}
}
}

在路由 router.js 脚本中,将登录接口抛出

1
2
3
4
5
6
7
8
9
10
'use strict';

/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.post('/api/user/register', controller.user.register);
router.post('/api/user/login', controller.user.login);
};

四、登录验证中间件

中间件我们可以理解成一个过滤器,举个例子,我们有 ABCD 四个接口是需要用户权限的,如果我们要判断是否有用户权限的话,就需要在这四个接口的控制层去判断用户是否登录。

每个接口都验证存在的弊端

1、每次编写新的接口,都要在方法内部做判断,这很费事。 2、一旦鉴权有所调整,我们需要修改每个用到判断登录的代码。

在请求接口的时候,过一层中间件,判断该请求是否是登录状态下发起的。此时我们打开项目,在 app 目录下新新建一个文件夹 middleware,并且在该目录下新增 jwtErr.js

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
'use strict';

module.exports = (secret) => {
return async function jwtErr(ctx, next) {
const token = ctx.request.header.authorization; // 若是没有 token,返回的是 null 字符串
let decode
if(token != 'null' && token) {
try {
decode = ctx.app.jwt.verify(token, secret); // 验证token
await next();
} catch (error) {
console.log('error', error)
ctx.status = 200;
ctx.body = {
msg: 'token已过期,请重新登录',
code: 401,
}
return;
}
} else {
ctx.status = 200;
ctx.body = {
code: 401,
msg: 'token不存在',
};
return;
}
}
}

首先中间件默认抛出一个函数,该函数返回一个异步方法 jwtErrjewErr 方法有两个参数 ctx 是上下文,可以在 ctx 中拿到全局对象 app

首先,通过 ctx.request.header.authorization 获取到请求头中的 authorization 属性,它便是我们请求接口是携带的 token 值,如果没有携带 token,该值为字符串 null。我们通过 if 语句判断如果有 token 的情况下,使用 ctx.app.jwt.verify 方法验证该 token 是否存在并且有效,如果是存在且有效,则通过验证 await next() 继续执行后续的接口逻辑。否则判断是失效还是不存在该 token

中间件完成后,我们在路由中router.js 去使用它

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

/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller, middleware } = app;
const _jwt = middleware.jwtErr(app.config.jwt.secret); // 传入加密字符串
router.post('/api/user/register', controller.user.register);
router.post('/api/user/login', controller.user.login);
router.get('/api/user/test', _jwt, controller.user.test); // 放入第二个参数,作为中间件过滤项
};