目录
在一个后台服务中,用户鉴权是一个重点,几乎所有的需求都是围绕用户(token)去进行设计的,所以在用户鉴权也是我们必须要重视的课程,本章我们就利用 egg-jwt 实现中间件,用作用户鉴权
什么是用户鉴权
引用百度百科对「用户鉴权」的定义:
用户鉴权,一种用于在通信网络中对试图访问来自服务提供商的服务的用户进行鉴权的方法。用于用户登陆到DSMP或使用数据业务时,业务网关或Portal发送此消息到DSMP,对该用户使用数据业务的合法性和有效性(状态是否为激活)进行检查。
将复杂的东西简单化。简单来说,鉴权就是用户在浏览网页或 App
时,通过约定好的方式,让网页和用户建立起一种相互信赖的机制,继而返回给用户需要的信息。
鉴权的机制,分为四种:
-
HTTP Basic Authentication
-
session-cookie
-
Token 令牌
-
OAuth(开放授权)
一般来说,我们大多数后台都是使用的是 token令牌模式,token也可以运用在如网页,客户端,小程序,游览器插件等领域。如果使用cookie鉴权,则在客户端和小程序下就无法使用这套接口。因为他们没有域的概念。而cookie则是要存在于某个域。
注册接口
开发过后台或者小程序的人会知道,注册接口一般流程是如下:
打开页面 - 输入用户名、密码 - 点击注册 - 服务器接收到后判断是否有重 - 没有重就将用户名及密码保存进用户表 - 返回注册成功信息
首先,我们需要新建一个 egg项目。不记得的话大家可以翻看第一章节。
然后我们需要配置mysql插件,在 config/config.defaul.js 中,将数据库改为自己的数据库以及对应的用户信息表。
我在这里的用户表叫user,表内容如下:
其它的项目配置与之前章节类似,大家忘了的记得查看一下。我在这里就不多说了。
mysql 配置之前记得下载 egg-mtsql 插件哦。
正式编写
根据刚开始一样,我们知道在注册的时候,前端会传递用户名与密码到后台来,所以我们需要在服务端拿到这两个值
-
我们需要现在controller目录下新建user.js 用于编写相关代码。
// controller/user.js
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 是否为空
if (!username || !password) {
ctx.body = {
code: 500,
msg: '账号密码不能为空',
data: null
}
return
}
-
判断是否重名,重名判断这里,分为两步。
第一步需要写service,因为我们要去接口里进行查询,是否有这个值。
第二部则写的是controller,用于判断。
// service/user.js
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;
// 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
}
}
经过两层判断,下来我们就可以将用户名密码插入数据库。
// controller/user.js
const defaultAvatar = '默认图片地址'
// 调用 service 方法,将数据存入数据库。
const result = await ctx.service.user.register({
username,
password,
signature: '个性签名',
avatar: defaultAvatar,
ctime: new Date(),
});
if (result) {
ctx.body = {
code: 200,
msg: '注册成功',
data: null
}
} else {
ctx.body = {
code: 500,
msg: '注册失败',
data: null
}
}
-
补充service 插入数据库方法
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。
router.post('/api/user/register', controller.user.register);
完成以上后,我们可以使用postman进行尝试
调用接口,显示成功,我们再看下数据库
同样也插进来了。
于此同时,我们可以尝试下之前两个判断。
第一个判断是,username 与 password 任意一个为空判断
设置password为空后,返回正确。
下来判断下用户名重复问题。
因为之前已经插入过相同用户名的数据,所以再次插入则会显示已存在。两个判断结束。
注册接口结束。
登录接口
完成注册之后,用户紧跟着是要用已成功的用户名密码进行登录,也就是我们的登录流程,登录成功后会返回一个token,这个令牌,我们结合 egg-jwt 插件,再加上自定义的加密字符串生成。
一般来说,前端获取到token后,会将其存在于前端保存(游览器本地),但是它要有过期时间,通常我们设置过期时间为24小事,如果不是一些信息敏感的网站或者app,我们可以设置时间更长一点。
令牌就是,每次发起请求,无论是否获取数据,还是提交数据,我们都需要将令牌带上,以此来标识这次请求是哪一个的用户的行为。
egg-jwt 有个特点就是,它既可以加密生成token,也可以解密token获取用户信息。而token大概就是登录用户的基本信息。
打个比方来说,你登录用户用户名密码是
{
userName:'赵小左',
password: '123',
}
那么,你的token就会含有以上用户信息,解密出来自然就是对应 username 等于 赵小左的基本信息。
安装 egg-jwt
npm install egg-jwt -S
添加插件到egg
// config/plugin.js
jwt: {
enable: true,
package: 'egg-jwt'
}
配置自定义加密字符串
// config/config.default.js
config.jwt = {
secret: 'Nick',
};
secret
加密字符串,将在后续用于结合用户信息生成一串 token
。secret
是放在服务端代码中,普通用户是无法通过浏览器发现的,所以千万不能将其泄漏,否则有可能会被不怀好意的人加以利用。
添加 请求响应的login 方法
login中要判断两个条件。
第一个是用户名是否存在
第二个是密码是否正确
// controller/user.js
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
}
}
判断完后则就可以开始正式login
async login () {
...
// 生成 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
},
};
}
这里使用app.jwt.sign 是因为app是全局上下文的一个熟悉,config的插件, config.default.js 中的属性,我们都可以通过 app.xxx,以及 app.config.xx 获取得到。
我们在登录的时候,将userinfo的id 和 username 两个属性通过jwt的 sign方法结合 jwt.secret 加密字符串,生成了一个token,这个token会是一个很长的字符串。
完成上述之后,我们就可以在router中将登录接口api 抛出。
router.post('/api/user/login', controller.user.login);
我们下来可以打开postman进行调用尝试
第一步老样子,先来验证两个判断。
-
用户名不存在判断(输入错误的用户名)
验证通过
-
密码错误判断(输入错误密码)
也验证通过。那么接下来我们就输入正式的账号密码进行测试
正式用户名密码输入,返回token ,系统登录成功,接口验证通过。
jwt解密token
上述我们使用登录完成了token生成。这时候我们如果有一个接口,需要令牌才能登录,那登录后我们如何来解析token获取用户信息呢?
我们在controller 中新增一个 方法 你可以叫它 test
// controller/user.js
async test() {
const {ctx,app} = this;
const token = ctx.request.header.token; // 获取header 的token
const decode = app.config.jwt.verify(token, app.config.jwt.secret);
ctx.body = {
code: 200,
msg: '成功',
data:{
...decode
}
}
}
别忘了增加 router方法
router.get('/api/user/getinfo', controller.user.text)
最后用postman进行调用
完整展现出来。
实现鉴权中间件
在上述 ,我们已经成功的将token实现出来了,并且也已经解析出来。
当然了,我们要的一个鉴权是在每一个需求token的接口,都需要鉴权。
我们不能跟之前一样死板的使用if else 来进行。这样你写多少个接口就得有多少判断。这样第一是死板,我们的接口写多少就得有多少个判断token。第二就是不易扩展,如果后面我们的token发生变化怎么办,每个接口都得改一遍吗?
那怎么办呢?实现鉴权中间件。在每次请求接口的时候,都先通过中间件去判断
新建中间件 middleware
我们先在项目的app下新建文件夹 middleware,并在下面增加jwtErr.js文件
其次我们在 jwtErr.js 中添加判断方法。
module.exports = (secret) => {
return async function jwtErr(ctx, next) {
const token = ctx.request.header.token; // 若是没有 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;
}
}
}
首先中间件默认抛出一个函数,该函数返回一个异步方法 jwtErr
,jewErr
方法有两个参数 ctx
是上下文,可以在 ctx
中拿到全局对象 app
。
首先,通过 ctx.request.header.token 获取到请求头中的
token属性,它便是我们请求接口是携带的
token值,如果没有携带
token,该值为字符串
null。我们通过
if语句判断如果有
token的情况下,使用
ctx.app.jwt.verify方法验证该
token是否存在并且有效,如果是存在且有效,则通过验证
await next()继续执行后续的接口逻辑。否则判断是失效还是不存在该
token`。
然后我们在router 进行配置
// app/router.js
module.exports = app => {
const { router,controller,middleware } = app;
const _jwt = middleware.jwtErr(app.config.jwt.secret); 传入加密字符串
router.post('/api/user/getinfo', _jwt, controller.user.test ) // / 放入第二个参数,作为中间件过滤项。判断第二个参数的接口是否有jwt。可以在进入接口逻辑之前就进行判断
}
可以在postman上进行运行尝试
-
先模仿带有token的接口
显示正确。下来我们再模仿不带token的接口
显示也是正确的。所以鉴权就是这样。我们在 router 增加第二个参数,添加 _jwt
方法,这样便可在进入接口逻辑之前就进行用户权限的判断。后续需要jwt判断的接口都如上所示即可。