一、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
| '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;
|
此时我们拿到了 username
和 password
,我们需要判断两个参数是否为空。如果是空,则返回错误信息:
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
| '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
| 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
|
const defaultAvatar = 'http://s.yezgea02.com/1615973940679/WeChat77d6d2ac093e247c361f0b8a7aeb6c2a.png'
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
| ...
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
| 'use strict';
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插件
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() { const { ctx, app } = this; const { username, password } = ctx.request.body 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 } const token = app.jwt.sign({ id: userInfo.id, username: userInfo.username, exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) }, app.config.jwt.secret);
ctx.body = { code: 200, message: '登录成功', data: { token }, }; }
|
把获取到的 userInfo
中的 id
和 username
两个属性,通过 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; const token = ctx.request.header.authorization; 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';
module.exports = app => { const { router, controller } = app; router.post('/api/user/register', controller.user.register); router.post('/api/user/login', controller.user.login); };
|
四、登录验证中间件
中间件我们可以理解成一个过滤器,举个例子,我们有 A
、B
、C
、D
四个接口是需要用户权限的,如果我们要判断是否有用户权限的话,就需要在这四个接口的控制层去判断用户是否登录。
每个接口都验证存在的弊端
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; let decode if(token != 'null' && token) { try { decode = ctx.app.jwt.verify(token, secret); 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; } } }
|
首先中间件默认抛出一个函数,该函数返回一个异步方法 jwtErr
,jewErr
方法有两个参数 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';
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); };
|