本帖最后由 菜肉果蔬 于 2018-11-26 16:23 编辑
一、JWT简介
JWT是json web token缩写。是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示,登录时发送如下信息:
{
"username": "源码时代",
"password": "itsource.cn",
}
服务器收到请求后,如果信息正确,可以到数据库查询到相关用户的所有信息,如下示例:
{
"username": "源码时代",
"password": "itsource.cn",
"role": "管理员",
"major": "Java",
"status": "在职",
}
这时,服务器可以把该用户的详细信息,通过密钥进行加密,生成一个token并返回给用户。用户可以把该token保存到客户端,服务器不保存任何用户信息。之后,当用户与服务器通信时,通常将token通过HTTP的Authorization header发送给服务端,服务端使用自己保存的密钥验证token的正确性,只要正确即通过验证。服务器仅依赖于这个token来标识用户。
1、jwt的组成(了解)
一个token分3部分,按顺序为
头部(header)
消息体(payload)
签证(signature)
由三部分生成token,3部分之间用“.”号做分隔。例如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2、头部(header)
头信息指定了该JWT使用的签名算法:
header = '{"alg":"HS256","typ":"JWT"}'
HS256 表示使用了 HMAC-SHA256 来生成签名。
其他的一下算法:
图片1.png (68.65 KB, 下载次数: 43)
2018-11-26 16:13 上传
3、消息体(payload)
消息体就是存放有效信息的地方。基本上填2种类型数据
-标准中注册的声明的数据-自定义数据
标准中注册的声明 (建议但不强制使用)
{
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
}
自定义数据
{
"username": "源码时代",
"password": "itsource.cn"
}
4、签名signature
未签名的令牌由base64url编码的头信息和消息体拼接而成(使用"."分隔),签名则通过私有的key计算而成:
key = 'secretkey'
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)
signature = HMAC-SHA256(key, unsignedToken)
最后在未签名的令牌尾部拼接上base64url编码的签名(同样使用"."分隔)就是JWT了:
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
# token看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI
token常常被用作保护服务端的资源(resource),客户端通常将token通过HTTP的Authorization header发送给服务端,服务端使用自己保存的key计算、验证签名以判断该token是否可信:
Authorization: 'Bearer eyJhbGciyu5CSpyHI'
二、JWT的使用
使用环境: 使用nodeJs express作为后台的技术,使用express-generator脚手架快速搭建后台服务器
以下所有使用到npm的地方推荐使用cnpm或者yarn替代
1、安装脚手架
npm install express-generator -g
2、构建项目
express myapp --view=pug
cd myapp
npm install
3、添加nodemon
在开发的时候,每次修改文件,都需要重启 express 服务,比较麻烦。使用nodemon,修改文件后可以自动重启 express 服务。
npm install --save-dev nodemon
修改 package.json 的 scripts 内容:
"scripts": {
"start": "node ./bin/www",
"devstart": "nodemon ./bin/www"
},
之后,使用npm run devstart 启动 express 服务。这样在开发过程中修改文件的时候,express服务就会自动重启,非常方便。
4、安装Jwt相关依赖
npm install jsonwebtoken --save
npm install express-jwt --save
express-jwt是nodejs的一个中间件,他来验证指定http请求的JsonWebTokens的有效性,如果有效就将JsonWebTokens的值设置到req.user里面,然后路由到相应的router。 此模块允许您使用Node.js应用程序中的JWT令牌来验证HTTP请求。 JWT通常用于保护API端点
express-jwt和jsonwebtoken是什么关系
express-jwt内部引用了jsonwebtoken,对其封装使用。 在实际的项目中这两个都需要引用,他们两个的定位不一样。jsonwebtoken是用来生成token给客户端的,express-jwt是用来验证token的。
5、设置需要保护的API
在app.js里面添加下面的内容
varexpressJWT = require('express-jwt');
varsecretOrPrivateKey = "itsource.cn";//秘钥,加密token 校验token时要使用
app.use(expressJWT({
"secret": secretOrPrivateKey, // 校验时用的秘钥
}).unless({
path: ['/getToken', '/login'] //除了这个地址,其他的URL都需要验证
}));
6、添加校验token失败时的处理
app.js里面添加蓝色部分的代码
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
// 当客户端请求时发过来的token无效或者过期的时候会触发该错误
if(err.name === 'UnauthorizedError') {
// 这个需要根据自己的业务逻辑来处理( 具体的err值 请看下面)
res.status(401).send('无效的token,请重新获取');
}
res.render('error');
});
7、定义获取token的接口
定义一个方法,在里面加密相应的用户信息,生成token并返给客户端,可以让客户端获取token。
在routes/index.js里面添加如下代码:
varjwt = require('jsonwebtoken');
varsecretOrPrivateKey = "itsource.cn";
router.get('/getToken', function(req, res) {
// 模拟数据库真实存在的用户信息, 真实项目中,应该去数据库查找
constuserInfo = {
"username": "源码时代",
"password": "itsource.cn",
"role": "管理员",
"major": "Java",
"status": "在职",
};
/*
* 生成token并返回
* userInfo:要加密的用户信息
* secretOrPrivateKey:用来加密的秘钥
* expiresIn:生成的token多久之后失效,单位秒
* */
consttoken = jwt.sign(userInfo, secretOrPrivateKey, { expiresIn: 60 * 60 * 10 });
res.json({
success: false,
data: token,
});
});
8、调用接口,生成token
这里使用poatman模拟客户端请求
请求方式:GET
如下:
图片2.png (52.76 KB, 下载次数: 49)
2018-11-26 16:14 上传
返回数据:
{
"success":true,"
data":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoi566h55CG5ZGYIiwibWFqb3IiOiJKYXZhIiwic3RhdHVzIjoi5Zyo6IGMIiwiaWF0IjoxNTQyMTcxMzE0LCJleHAiOjE1NDIyMDczMTR9.yhRSl0YqJ7Z1cXTBqxvurPInKVhOGoAeEaoTrPPhITo"
}
success: 表示此次请求是否成功
data:服务器返回的数据,这里是返回的 token
9、新增需要验证token的接口
第7步定义的接口,因为在app.js里面配置了路径,如下:
unless({
path: ['/getToken', '/login] //除了这里面的地址,其他的URL都需要验证
}));
所以请求的时候不需要验证token,但没有配置路径的接口则需要验证,这里我们可以新增一个接口验证我们之前返回的token是否有效。
在routes/index.js里面添加如下代码:
// 访问这个地址,token 要放到 authorization 这个header里,
// 对应的值以Bearer开头然后空一格,接近着是token值。为什么会这样,请看下面后续。
router.get('/getData', function(req, res) {
// 请求成功,可以从request里面获取之前jwt封装的用户的信息。
constuserInfo = req.user;
// 拿到用户信息以后可以在这里做一下额外的事情,比如说权限验证,根据用户的角色,判断他是否可以调用这个接口
if(userInfo.role !== '管理员') { // 不是管理员直接返回
res.json({
success: false,
message: '权限不够,管理员才有权限访问该接口',
})
}
res.json({
success: true,
message: '看到这个说明你访问成功了',
data: userInfo,
})
});
直接访问该接口:
请求方式:GET
如下:
图片3.png (49.61 KB, 下载次数: 51)
2018-11-26 16:15 上传
因为请求的时候没有携带token,所以请求会被直接拦截,无法访问到该接口。
设置token并访问该接口
请求方式:GET
图片4.png (96.36 KB, 下载次数: 45)
2018-11-26 16:16 上传
success: 表示此次请求是否成功
data:服务器返回的数据
请求的时候,在headers里面设置了authorization的值,所以可以访问到接口,并且可以从请求的request里面获取之前加密的用户信息,并返回。
10、客户端携带token正确的姿势
放到 authorization 这个header里, 对应的值以Bearer开头然后空一格
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoi566h55CG5ZGYIiwibWFqb3IiOiJKYXZhIiwic3RhdHVzIjoi5Zyo6IGMIiwiaWF0IjoxNTQyMTcyODg0LCJleHAiOjE1NDIyMDg4ODR9.Bn25XzMae5t_CNQyiZzLBuXfBYVBmhls1mvLMP210do
为什么会这样携带token,请看express-jwt源码里是如何获取token的:
//1 从options中获取token 这个忽略 因为 在设置 需要保护的API 时 并没有传递 getToken 这个方法
if(options.getToken && typeofoptions.getToken === 'function') {
try{
token = options.getToken(req);
} catch(e) {
returnnext(e);
}
//2 从authorization中获取token
} else if(req.headers && req.headers.authorization) {
// --这是关键代码-----开始切割--------->
varparts = req.headers.authorization.split(' ');
if(parts.length == 2) {
varscheme = parts[0];
varcredentials = parts[1];
if(/^Bearer$/i.test(scheme)) {
token = credentials; //
} else{
if(credentialsRequired) {
returnnext(newUnauthorizedError('credentials_bad_scheme', { message: 'Format is Authorization: Bearer [token]' }));
} else{
returnnext();
}
}
//3 以上两个途径都没有token时 就报错呗
} else{
returnnext(newUnauthorizedError('credentials_bad_format', { message: 'Format is Authorization: Bearer [token]' }));
}
}
11、express-jwt 作为中间件都做了什么
总体流程:
图片5.png (47.49 KB, 下载次数: 46)
5
2018-11-26 16:16 上传
12、前端框架中使用示例代码(以axios为例,根据实际情况调整,只要在请求的haders里面添加Authorization属性就可以, 值为‘Bearer ’ +后台返回的token)
varaxios =require('axios');
export default functiongetData() {
returnaxios.request({
url: 'http://localhost:3000/getData',
method: 'get',
headers: {
Authorization: 'Bearer eyJhbGciCJ9.eyJ1c2VwMjB9.vDDhhaLNYufXlQ'
} }).then((res) => {
if(res.status === 200) {
returnPromise.resolve({ success: true, data: res.data });
} else{
returnPromise.resolve({ success: false});
}
}).catch((err) => {
returnPromise.resolve({ success: false});
});
}
感谢源码时代教学讲师提供此文章!
本文为原创文章,转载请注明出处!