JWT(详解)

news/2024/12/28 11:45:32/

7.JWT

随着前后端分离的发展,以及数据中心的建立,越来越多的公司会创建一个中心服务器,服务于各种产品线。

而这些产品线上的产品,它们可能有着各种终端设备,包括但不仅限于浏览器、桌面应用、移动端应用、平板应用、甚至智能家居
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3yT7MhFL-1649293375396)(media/16481748421276/16481748624356.jpg)]

实际上,不同的产品线通常有自己的服务器,产品内部的数据一般和自己的服务器交互。

但中心服务器仍然有必要存在,因为同一家公司的产品总是会存在共享的数据,比如用户数据

这些设备与中心服务器之间会进行http通信

一般来说,中心服务器至少承担着认证和授权的功能,例如登录:各种设备发送消息到中心服务器,然后中心服务器响应一个身份令牌

当这种结构出现后,就出现一个问题:它们之间还能使用传统的cookie方式传递令牌信息吗?

其实,也是可以的,因为cookie在传输中无非是一个消息头而已,只不过浏览器对这个消息头有特殊处理罢了。

但浏览器之外的设备肯定不喜欢cookie,因为浏览器有着对cookie完善的管理机制,但是在其他设备上,就需要开发者自己手动处理了

jwt的出现就是为了解决这个问题

概述

jwt全称Json Web Token,强行翻译过来就是json格式的互联网令牌

它要解决的问题,就是为多种终端设备,提供统一的、安全的令牌格式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8NfqFv46-1649293375398)(media/16481748421276/16481749149910.jpg)]

因此,jwt只是一个令牌格式而已,你可以把它存储到cookie,也可以存储到localstorage,没有任何限制!

同样的,对于传输,你可以使用任何传输方式来传输jwt,一般来说,我们会使用消息头来传输它

比如,当登录成功后,服务器可以给客户端响应一个jwt:

HTTP/1.1 200 OK
...
set-cookie:token=jwt令牌
authorization:jwt令牌
...{..., token:jwt令牌}

可以看到,jwt令牌可以出现在响应的任何一个地方,客户端和服务器自行约定即可。

当然,它也可以出现在响应的多个地方,比如为了充分利用浏览器的cookie,同时为了照顾其他设备,也可以让jwt出现在set-cookieauthorization或body中,尽管这会增加额外的传输量。

当客户端拿到令牌后,它要做的只有一件事:存储它。

你可以存储到任何位置,比如手机文件、PC文件、localstorage、cookie

当后续请求发生时,你只需要将它作为请求的一部分发送到服务器即可。

虽然jwt没有明确要求应该如何附带到请求中,但通常我们会使用如下的格式:

GET /api/resources HTTP/1.1
...
authorization: bearer jwt令牌
...

这种格式是OAuth2附带token的一种规范格式

至于什么是OAuth2,那是另一个话题了

这样一来,服务器就能够收到这个令牌了,通过对令牌的验证,即可知道该令牌是否有效。

它们的完整交互流程是非常简单清晰的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6HIfY4L1-1649293375399)(media/16481748421276/16481749923201.jpg)]

令牌的组成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OUsHZR9a-1649293375400)(media/16481748421276/16481775415325.jpg)]

jwt网站,里面提供了header,payload与生存jwt的互相转换

为了保证令牌的安全性,jwt令牌由三个部分组成,分别是:

  1. header:令牌头部,记录了整个令牌的类型和签名算法
  2. payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
  3. signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改

它们组合而成的完整格式是:header.payload.signature

比如,一个完整的jwt令牌如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9.BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

它各个部分的值分别是:

  • header:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • payload:eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9
  • signature: BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

下面分别对每个部分进行说明

header

它是令牌头部,记录了整个令牌的类型和签名算法

它的格式是一个json对象,如下:

{"alg":"HS256","typ":"JWT"
}

该对象记录了:

  • alg:signature部分使用的签名算法,通常可以取两个值
    • HS256:一种对称加密算法,使用同一个秘钥对signature加密解密
    • RS256:一种非对称加密算法,使用私钥加密,公钥解密
  • typ:整个令牌的类型,固定写JWT即可

设置好了header之后,就可以生成header部分了

具体的生成方式及其简单,就是把header部分使用base64 url编码即可

base64 url不是一个加密算法,而是一种编码方式,它是在base64算法的基础上对+=/三个字符做出特殊处理的算法

base64是使用64个可打印字符来表示一个二进制数据,具体的做法参考百度百科

浏览器提供了btoa函数,可以完成这个操作:

window.btoa(JSON.stringify({"alg":"HS256","typ":"JWT"
}))
// 得到字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

同样的,浏览器也提供了atob函数,可以对其进行解码:

window.atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")
// 得到字符串:{"alg":"HS256","typ":"JWT"}

nodejs中没有提供这两个函数,可以安装第三方库atobbota搞定。或者,手动搞定

payload

这部分是jwt的主体信息,它仍然是一个JSON对象,它可以包含以下内容:

{"ss":"发行者","iat":"发布时间","exp":"到期时间","sub":"主题","aud":"听众","nbf":"在此之前不可用","jti":"JWT ID"
}

以上属性可以全写,也可以一个都不写,它只是一个规范。就算写了,也需要你在将来验证这个jwt令牌时手动处理才能发挥作用

上述属性表达的含义分别是:

  • ss:发行该jwt的是谁,可以写公司名字,也可以写服务名称
  • iat:该jwt的发放时间,通常写当前时间的时间戳
  • exp:该jwt的到期时间,通常写时间戳
  • sub:该jwt是用于干嘛的
  • aud:该jwt是发放给哪个终端的,可以是终端类型,也可以是用户名称,随意一点
  • nbf:一个时间点,在该时间点到达之前,这个令牌是不可用的
  • jti:jwt的唯一编号,设置此项的目的,主要是为了防止重放攻击(重放攻击是在某些场景下,用户使用之前的令牌发送到服务器,被服务器正确的识别,从而导致不可预期的行为发生)

可是到现在,看了半天,没有出现我想要写入的数据啊😂

当用户登陆成功之后,我可能需要把用户的一些信息写入到jwt令牌中,比如用户id、账号等等(密码就算了😳)

其实很简单,payload这一部分只是一个json对象而已,你可以向对象中加入任何想要加入的信息

比如,下面的json对象仍然是一个有效的payload

{"foo":"bar","iat":1587548215
}

foo: bar是我们自定义的信息,iat: 1587548215是jwt规范中的信息

最终,payload部分和header一样,需要通过base64 url编码得到:

window.btoa(JSON.stringify({"foo":"bar","iat":1587548215
}))
// 得到字符串:eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9

signature

这一部分是jwt的签名,正是它的存在,保证了整个jwt不被篡改

这部分的生成,是对前面两个部分的编码结果,按照头部指定的方式进行加密

比如:头部指定的加密方法是HS256,前面两部分的编码结果是eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9

则第三部分就是用对称加密算法HS256对字符串eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9进行加密,当然你得指定一个秘钥,比如shhhhh

HS256(`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9`, "shhhhh")
// 得到:BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

最终,将三部分组合在一起,就得到了完整的jwt

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9.BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

由于签名使用的秘钥保存在服务器,这样一来,客户端就无法伪造出签名,因为它拿不到秘钥。

换句话说,之所以说无法伪造jwt,就是因为第三部分的存在。

而前面两部分并没有加密,只是一个编码结果而已,可以认为几乎是明文传输

这不会造成太大的问题,因为既然用户登陆成功了,它当然有权力查看自己的用户信息

甚至在某些网站,用户的基本信息可以被任何人查看

你要保证的,是不要把敏感的信息存放到jwt中,比如密码

jwt的signature可以保证令牌不被伪造,那如何保证令牌不被篡改呢?

比如,某个用户登陆成功了,获得了jwt,但他人为的篡改了payload,比如把自己的账户余额修改为原来的两倍,然后重新编码出payload发送到服务器,服务器如何得知这些信息被篡改过了呢?

这就要说到令牌的验证了

令牌的验证

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ba4Qlc7N-1649293375401)(media/16481748421276/16481749923201.jpg)]

令牌在服务器组装完成后,会以任意的方式发送到客户端

客户端会把令牌保存起来,后续的请求会将令牌发送给服务器

而服务器需要验证令牌是否正确,如何验证呢?

首先,服务器要验证这个令牌是否被篡改过,验证方式非常简单,就是对header+payload用同样的秘钥和加密算法进行重新加密

然后把加密的结果和传入jwt的signature进行对比,如果完全相同,则表示前面两部分没有动过,就是自己颁发的,如果不同,肯定是被篡改过了。

传入的header.传入的payload.传入的signature
新的signature = header中的加密算法(传入的header.传入的payload, 秘钥)
验证:新的signature == 传入的signature

当令牌验证为没有被篡改后,服务器可以进行其他验证:比如是否过期、听众是否满足要求等等,这些就视情况而定了

注意:这些验证都需要服务器手动完成,没有哪个服务器会给你进行自动验证,当然,你可以借助第三方库来完成这些操作

其实上面叽叽歪歪那么多,如果不想看,总结一下就三点:

  • jwt本质上是一种令牌格式。它和终端设备无关,同样和服务器无关,甚至与如何传输无关,它只是规范了令牌的格式而已
  • jwt由三部分组成:header、payload、signature。主体信息在payload
  • jwt难以被篡改和伪造。这是因为有第三部分的签名存在。

在node中使用JWT

nodejs实现的JWT,我们需要借助第三方插件jsonwebtoken

安装

npm i jsonwebtoken

使用
主要方法:

生成JWT

jwt.sign(payload, secretOrPrivateKey, [options, callback])
  • payload 参数必须是一个object、Buffer、或 string.

  • 注意:exp(过期时间) 只有当payload是object字面量时才可以设置。如果payload不是buffer或string,它会被强制转换为使用的字符串JSON.stringify()。

  • secretOrPrivateKey 参数 是包含HMAC算法的密钥或RSA和ECDSA的PEM编码私钥的string或buffer。

  • options 参数有如下值:

    • algorithm:加密算法(默认值:HS256)
    • expiresIn:以秒表示或描述时间跨度zeit / ms的字符串。如60,“2 days”,“10h”,“7d”,含义是:过期时间
    • notBefore:以秒表示或描述时间跨度zeit / ms的字符串。如:60,“2days”,“10h”,“7d”
    • audience:Audience,观众
    • issuer:Issuer,发行者
    • jwtid:JWT ID
    • subject:Subject,主题

验证JWT

jwt.verify(token, secretOrPrivateKey, [options, callback])

示例

const jwt = require("jsonwebtoken");
const secret = "yingside";function createJWT(payload = {},maxAge = 60 * 60 * 24,){let token = jwt.sign(payload,secret,{expiresIn:maxAge});return token;
}function verifyToken(token){const result = jwt.verify(token,secret);return result;
}//生成JWT
let token = createJWT({name:"张三",id:20
},"2 days");console.log(token);//验证JWT
let result = verifyToken(token);
console.log(result);

jwt 认证流程

登录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kmjIGDH4-1649293375402)(media/16481748421276/16481995010523.jpg)]
① 用户登录时,将用户名和密码通过 POST 提交到服务端

② 服务端接收到登录数据之后,与数据库中的用户信息进行校验

③ 校验通过

④ 服务端通过用户 _id,secret 等等相关数据生成 JWT 字符串

⑤ 将 JWT 字符串和其他信息一起返回给浏览器,浏览器拿到 jwt 字符串后,缓存到本地,等待下一次请求

下一次请求

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OSonZpwJ-1649293375403)(media/16481748421276/16481996172401.jpg)]

① 用户登录之后的每次请求,都会将 token 携带在请求头的 Authorization 中

② 对 token 验证成功之后,我们可以拿到保存在 token 中的用户数据

③ 进入业务处理

示例Demo

前端用户登录时页面 login.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><form action="/api/user/login" method="post"><input type="text" name="loginId" id="loginId"> <br><input type="password" name="loginPwd" id="loginPwd"> <br><input type="button" value="登录" id="btn"></form><br><button id="btnQuery">点击访问后台数据</button>
</body>
<script src="js/axios.js"></script>
<script src="js/jquery.min.js"></script>
<script src="js/login.js"></script>
</body>
</html>

服务器需要生成和验证jwt的工具,编写jwt.js代码

const jwt = require("jsonwebtoken");
const secret = "yingside";function createJWT(payload = {},maxAge = 60 * 60 * 24,){let token = jwt.sign(payload,secret,{expiresIn:maxAge});return token;
}function verifyToken(token){const result = jwt.verify(token,secret);return result;
}let token = createJWT({name:"张三",id:20
},"2 days");console.log(token);let result = verifyToken(token);
console.log(result);//发布jwt
module.exports.publish = (res,payload = {},maxAge = 60 * 60 * 24,) => {let token = createJWT(payload,maxAge);res.header('authorization',token);return token;
}
//验证jwt
module.exports.verify = (req)=>{let token = req.headers.authorization;console.log(req.headers.authorization);if(!token){return null;}//格式:authorization bearer tokentoken = token.split(" ");token = token.length === 1 ? token[0] : token[1];try{let result = verifyToken(token);console.log("----->" + result);return result;}catch(err){return null;} 
}

当用户登录时,在后端路由生成jwt的token

router.post("/login",async(req,res)=>{console.log("=======> login");let result = await UserService.login(req.body.loginId,req.body.loginPwd);let token;if (result) {let value = result.id;//登录成功token = jwt.publish(res, { id: value });console.log("----->" + token);}res.json(token);
})

前端登录发送ajax请求,成功后获取后端传送过来的token,并保存在headers的authorization属性中

login.js

$("#btn").on("click",function(){$.ajax({url:"/api/user/login",method:"post",data:{"loginId":$("#loginId").val(),"loginPwd":$("#loginPwd").val()},success:function(data,txt,xhr){localStorage.setItem("token", xhr.getResponseHeader("authorization"));   }});
})

自此,前端浏览器 在headers的authorization中保存了jwt字符串,以后每次请求后端服务器的时候,都会从header中带着这个jwt字符串,后端路由验证jwt是否存在并正确,以此给每次请求授权
tokenMiddleware.js

const pathToRegexp = require("path-to-regexp");
const jwt = require("./jwt");
// 设置需要token验证的URI
let needToToken = [{ method: "POST", path: "/api/user" },{ method: "GET", path: "/api/user/:id" },
];//由于/api/user/:id真正对应的是/api/user/123这样的URI路径
//因此,为了匹配是否URI路径对应,使用path-to-regexp模块
module.exports = function(req, res, next){//过滤出当前URI模块是否需要token认证const apis = needToToken.filter((api) => {let reg = pathToRegexp(api.path);return api.method === req.method && reg.test(req.path);});//如果没有,不要认证,直接放行if (apis.length == 0) {next();return;}//验证jwt是否正确  const result = jwt.verify(req);if (result) {//认证通过req.loginId = result.id;next();} else{//注意这里需要错误处理中间件throw new Error("权限认证未通过");}
};

错误处理中间件(注意要放在最后)

// catch 404 and forward to error handler
app.use(function(req, res, next) {next(createError(404,"你访问的页面不存在"));
});// 错误处理中间件,有Error时触发该中间件
app.use(function(err, req, res, next) {// set locals, only providing error in developmentres.locals.message = err.message;res.locals.error = req.app.get('env') === 'development' ? err : {};// render the error page//res.status(err.status || 500);//res.render("error");res.json(err.message);
});

客户端模拟访问需要权限控制的页面

//每次jquery访问之前,读取localstorage中保存的jwt字符串,并设置到header中
//这样每次请求都会带上这个头信息
$.ajaxSetup({beforeSend: function(xhr) {const token = window.localStorage.getItem('token');xhr.setRequestHeader('Authorization', `Bearer ${token}`);}
});//模拟访问需要权限控制的页面
$("#btnQuery").on("click",function(){$.ajax({url:"/api/user/11",method:"get",success:function(data,txt,xhr){console.log(data);}});
})

http://www.ppmy.cn/news/315444.html

相关文章

JTW

1.1.1.简介 JWT&#xff0c;全称是Json Web Token&#xff0c; 是JSON风格轻量级的授权和身份认证规范&#xff0c;可实现无状态、分布式的Web应用授权&#xff1b;官网&#xff1a;https://jwt.io 1.1.2.数据格式 JWT包含三部分数据&#xff1a; Header&#xff1a;头部&…

JWT技术

JWT 一、 JWT 实现无状态 Web 服务 1、什么是有状态 有状态服务&#xff0c;即服务端需要记录每次会话的客户端信息&#xff0c;从而识别客户端身份&#xff0c;根据用户身份进行请求的处理&#xff0c;典型的设计如tomcat中的session。 例如登录&#xff1a;用户登录后&am…

JTS简介

地理坐标系 球面坐标系&#xff0c;以度为单位&#xff0c;比如WGS84等等投影坐标系&#xff0c;以米为单位&#xff0c;比如墨卡托投影&#xff0c;高斯投影&#xff0c;UTM投影等等 SRID 国际标准下坐标系唯一ID&#xff0c;比如WGS84 4326&#xff0c;墨卡托投影 3857 …

【Hadoop】环境准备

克隆虚拟机修改克隆虚拟机的静态IP修改主机名关闭防火墙创建jzt用户配置jzt用户具有root权限 注意 : 在克隆之前,母机需要设置好hosts映射,不然到时候每台主机去配置主机映射较麻烦 母机的主机映射文件hosts: 克隆虚拟机 修改克隆虚拟机的静态IP 将ip改了,并将硬件地址粘贴过…

史上最详细的安装Kali-linux教程(附视频教程)

之前不少人问kali怎么安装&#xff0c;今天就发一篇利用VM虚拟机安装kali的详细教程&#xff0c;每一步都截图了&#xff0c;让大家尽可能的清楚每一步的操作。 1.2 使用 VM 虚拟机安装 Kali 1.2.1 官方下载 Kali Linux 官方网址&#xff1a;http://www.Kali.org 下载方式分…

传感器是指纹识别产品的数据入口

苹果公司对指纹识别传感器供应商美国AuthenTec公司的收购&#xff0c;开启了指纹识别在智能手机领域大规模应用的先河。 华为Mate 7手机的热卖正式引爆了指纹识别在中高档智能手机中的普及&#xff0c;其准确快速的识别体验让瑞典Fingerprint Cards AB(FPC)公司一炮而红。 成为…

解决联通手机营业厅部分页面无法打开(打开空白 进不去 闪退 排版错乱)的问题.2021-02-26

如果手机系统的版本低于 安卓5 ,不用往下看了,换个手机吧.方法只适合安卓5及更高版本的系统 一.故障复现 使用的设备: 酷派锋尚pro2全网通版(系统UI:coolui8.0,安卓版本5.1.1) 出现故障的软件: 联通手机营业厅8.0102(其它版本也有这个问题) 打开手厅首页上的 剩余话费 或…

酷派+乐视能否再造一个“苹果生态”

6月30日&#xff0c;乐视创始人贾跃亭在微博称&#xff1a;“生态化反&#xff0c;开放共享。乐视移动智能公司成为酷派第二大股东&#xff0c;开放闭环的乐视生态将推动酷派快速进入手机生态时代&#xff0c;酷派深厚的产研供销、售后能力与乐视强化反&#xff0c;全流程共生共…