什么是JWT
JWT——Json web token
JWT——Json web token 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准,可实现无状态、分布式的Web应用授权。
为什么需要JWT
应用于前后端分离的项目,后端接口部署在分布式环境下
传统的session认证
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.
- cookie 不能跨域
- 前端:不是在专用服务器,而是当访问的时候会给一个cdn或域名
- 后端:部署在专用服务器,使用一个域名
- 多端 PC端、手机、webApp、App、小程序
- App 、小程序 不支持cookie、session
- 分布式环境下session不能共享
- session工作在服务器端,把用户的信息记录在服务器
- 需要想办法将用户的信息在多台服务器间共享
基于token的鉴权机制
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
token => 字符串 => 存储在客户端 (本地存储 sessionstorage、localstorage) => 发送请求 手动携带token
流程上是这样的:
-
用户使用用户名密码来请求服务器
-
服务器进行验证用户的信息 服务器通过验证发送给用户一个token
-
客户端存储token,并在每次请求时附送上这个token值 服务端验证token值,并返回数据
-
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了
Access-Control-Allow-Origin:*
。
也就是说相比于传统基于cookie的session,基于token的jwt有以下优点: -
Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
-
无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
-
更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
-
去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
-
更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
-
CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
-
性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
-
不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
-
基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
Json Web Token(JWT)
JWT 用于身份认证、可用于客户端与服务器 传递 用户的不敏感数据
JWT组成:
Herder.payload.Signature
herder(头)、payload(负载)、Signature(签名)
如何使用JWT
- 用户账号、密码 验证成功 生成token 发送给客户端并保存token
- 客户端每次ajax请求,携带token 发给服务器做验证
- get查询字符串
- header 请求头
- body 请求体
- 服务器验证 在需要身份验证的接口,进行token的验证,通过 继续 返回业务逻辑结果 | 未通过 返回身份认证失败
JWT身份认证操作
环境:Node.js Vue2.0项目 Egg.js
插件:egg-jwt:www.npmjs.com/package/egg…
egg-validate-plus-next:www.npmjs.com/package/egg…
安装插件
在终端分别输入以下两条命令进行插件安装:
npm i egg-validate-plus-next
npm i egg-jwt --save
egg-validate-plus-next配置
开启插件
// config/plugin.js
module.exports = {
// 验证规则插件开启
validatePlusNext: {
enable: true,
package: 'egg-validate-plus-next',
},
}
配置插件
// config/config.default.js
const userConfig = {
// 验证规则插件配置
validatePlusNext: {
resolveError(ctx, errors) {
if (errors.length) {
ctx.type = 'json';
ctx.status = 200; // 修改http响应的状态码
ctx.body = {
code: 404,
error: errors,
msg: '参数错误',
};
}
},
},
}
使用插件为登录模块定义验证规则
可以写在app/validate
目录下也可以直接写在控制器
// app/controller/login.js
const rules = {
username: { type: 'string', required: true, max: 10, message: '用户名不能为空' },
password: { type: 'string', required: true, max: 10, message: '密码不能为空' },
};
const valid = await this.ctx.validate(rules, this.ctx.request.body, 'rule');
if (!valid) return;
egg-jwt配置
// /config/plugin.js
module.exports = {
jwt: {
enable: true,
package: 'egg-jwt',
},
}
// config/config.default.js
const userConfig = {
jwt: {
secret: 'jiaju2022', // Signature 签名 秘钥
},
}
注册路由
// app/router.js
router.post('login', '/api/login', controller.login.checkLogin); // 登录
注册控制器
// app/controller/login.js
async checkLogin() {
// 内容验证
const rules = {
username: { type: 'string', required: true, max: 10, message: '用户名不能为空' },
password: { type: 'string', required: true, max: 10, message: '密码不能为空' },
};
const valid = await this.ctx.validate(rules, this.ctx.request.body, 'rule');
if (!valid) return;
// 调用服务进行提交到后台验证
const token = await this.ctx.service.login.checkLogin(this.ctx.request.body);
this.ctx.body = {
code: 200,
msg: '登录成功',
data: token,
};
}
步骤:
- 当前台通过post提交用户名和密码,
this.ctx.validate
调用验证规则进行数据验证 - 如果验证不通过,则返回验证规则中的提示信息
- 验证通过则继续向下执行,控制器会通过调用服务,并将前台提交过来的用户名密码等信息传递到服务
- 当服务给予响应以后通过
this.ctx.body
返回结果
服务
// app/service/login.js
async checkLogin(data) {
const { username, password } = data;
const user = await this.ctx.model.Admin.findOne(data, {
where: {
username,
},
});
if (username) {
if (user.password === this.ctx.helper.pwd(password)) {
const token = this.app.jwt.sign({ username }, this.app.config.jwt.secret);
return { token };
} else if (user.password !== this.ctx.helper.pwd(password)) {
this.ctx.throw(200, '密码错误');
}
} else {
this.ctx.throw(200, '用户名不存在');
}
}
步骤:
- 当接收到控制器传递回来的参数,通过
this.ctx.model.Admin.findOne()
方法调用数据库,并设置查询条件为前台传递的用户名(username) - 进行判断,当用户名存在,则进行判断前台传递的密码与后台数据库查到的用户名所匹配的密码是否一致,不一致则提示用户名不存在
- 当密码一致的时候设置token并return返回,否则提示密码错误