- 上次遇到jwt还是打jwt,现在开发也遇到了
jwt简介
-
jwt是一组来进行鉴权的口令,和以往的session和cookie不同的是jwt不存于服务端,jwt在用户认证成功(比如登录)后会被生成,然后通过setcookie发给客户端(比如前端)让客户端带有这个参数.既然只存于客户端,那么客户端可以伪造或者修改jwt吗?这个问题就得从jwt的结构来理解
-
.JWT分别由标头(Header)、有效载荷(Payload)和签名(Signature)三个部分组成,采用base64url编码进行加密,以.作为连接的字符串形式。
-
把前两段的base密文通过
.
拼接起来,然后对其进行加密,加密方式为标头声明的方式,再然后对加密的密文进行base64url加密,最终得到token的第三段。
eyJraWQiOiJrZXlzLzNjM2MyZWExYzNmMTEzZjY0OWRjOTM4OWRkNzFiODUxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.e
yJzdWIiOiJkdWJoZTEyMyJ9.XicP4pq_WIF2bAVtPmAlWIvAUad_eeBhDOQe2MXwHrE8a7930LlfQq1lFqBs0wLMhht6Z9BQXBRo
s9jvQ7eumEUFWFYKRZfu9POTOEE79wxNwTxGdHc5VidvrwiytkRMtGKIyhbv68duFPI68Qnzh0z0M7t5LkEDvNivfOrxdxwb7IQs
AuenKzF67Z6UArbZE8odNZAA9IYaWHeh1b4OUG0OPM3saXYSG-
Q1R5X_5nlWogHHYwy2kD9v4nk1BaQ5kHJIl8B3Nc77gVIIVvzI9N_klPcX5xsuw9SsUfr9d99kaKyMUSXxeiZVM-7os_dw3ttz2
f-TJSNI0DYprHHLFw
header
- header部分承载两部分信息: 一个是typ,表示令牌类型 一个是alg,表示签名所使用的算法,默认是 HMAC SHA256.header是一段如下的json,这里面的键是不固定的,理论上写本书进来都行
{
"kid": "keys/3c3c2ea1c3f113f649dc9389dd71b851",
"typ": "JWT",
"alg": "RS256"
}
- 将其进行base64url编码,作为第一部分,并且加上.与第二部分分开,得到
eyJraWQiOiJrZXlzLzNjM2MyZWExYzNmMTEzZjY0OWRjOTM4OWRkNzFiODUxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.
payload
- payload部分是JWT的主体部分,用于存放有效数据。包含三个部分,标准中注册的声明,公共的声明,私有的声明.包含以下内容,这里的内容也不是固定的,记录一些想要记录的信息,比如用户的用户名或者密码之类的,保证服务端解码之后可以利用里面存储的信息验证用户即可
"payload": {
"iss": "some auth server", // 颁发token的 auth server
"sub": "naimish", //主题是什么,通常用来指定颁发给哪个user
"aud": "myrest client", //确定要给哪个resource server 验证 token
"iat": 1681480232, // 发布时间
"exp": 1682480832, //到期时间
"data": [ //自定义的数据
{
"aaa": "something ",
"bbb": "other",
}
],
}
{
"sub": "dubhe123"
}
- 将其进行base64url编码,作为第二部分,并且加上.与第三部分分开,得到
eyJzdWIiOiJkdWJoZTEyMyJ9.
signature
- signature原信息本身是一段加密算法,原信息是按照下面的格式根据header和payload推出来的,但是写入到jwt的部分是这段代码进行加密的结果.
- 原信息模板为
base64url(
加密方式SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your-256-bit-secret (秘钥加盐)
)
)
- 比如根据已知header,加密方式为HMAC,则加密原文为
base64url(
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
随便填的密钥
)
)
- 带入header和payload得到密文
XicP4pq_WIF2bAVtPmAlWIvAUad_eeBhDOQe2MXwHrE8a7930LlfQq1lFqBs0wLMhht6Z9BQXBRos9jvQ7eumEUFWFYKRZfu9POT
OEE79wxNwTxGdHc5VidvrwiytkRMtGKIyhbv68duFPI68Qnzh0z0M7t5LkEDvNivfOrxdxwb7IQsAuenKzF67Z6UArbZE8odNZAA
9IYaWHeh1b4OUG0OPM3saXYSG-
Q1R5X_5nlWogHHYwy2kD9v4nk1BaQ5kHJIl8B3Nc77gVIIVvzI9N_klPcX5xsuw9SsUfr9d99kaKyMUSXxeiZVM-7os_dw3ttz2f
-TJSNI0DYprHHLFw
- 也就是说,jwt的安全性来自于签名,我们可以伪造一段jwt,但是由于HMACSHA256需要一个密钥,没有服务端自己的密钥,我们就无法对伪造的信息进行一个"合理"的签名,显然我们自己随便写的密钥签的名丢给服务端后服务端是无法解签的,就会被判定为非法
- 那么修改现有的payload部分呢,当然也是不行的,虽然payload只是简单的进行了转码而不是加密,可以轻松修改,而签名来自于服务端也能签发成功,但是服务端验证jwt时只要对payload再次进行签名,就会发现两次的签名对不上,从而得知jwt被篡改
jwt的使用
-
上面的攻击思路提醒我们,在验证jwt时既要验证签名是否能够正常解签,也要验证payload的签名是否和signature对的上
-
这里提供了php和python对于jwt的签发和验证,jwt的签发和验证最好都是由后端发出,前端传jwt很容易被破解,即使你通过各种js加密技术,也容易被破解,比如著名的知乎x-zse-96参数逆向,和jwt类似,也就是说,对于前后端分离的项目,纯前端的部分也是需要一个后端的controller的存在才能使用的
-
python生成和验证jwt的示例
import time
import jwt
JWT_TOKEN_EXPIRE_TIME = 3600 * 2 # token有效时间 2小时
JWT_SECRET = 'abc' # 加解密密钥
JWT_ALGORITHM = 'HS256' # 加解密算法
def generate_jwt_token(user_id: int)->str:
"""根据用户id生成token"""
payload = {'user_id': user_id, 'exp': int(time.time()) + JWT_TOKEN_EXPIRE_TIME}
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
return token
def verify_jwt_token(user_id: int, token: str)->bool:
"""验证用户token"""
payload = {'user_id': user_id}
try:
_payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
except jwt.PyJWTError:
print('token解析失败')
return False
else:
print(_payload)
exp = int(_payload.pop('exp'))
if time.time() > exp:
print('已失效')
return False
return payload == _payload
if __name__ == '__main__':
user_id = 'abc'
token = generate_jwt_token(user_id)
print(token)
print(verify_jwt_token(user_id, token))
- 这里通过验证exp键来判断jwt是否过期
exp = int(_payload.pop('exp'))
if time.time() > exp:
print('已失效')
return False
- 然后验证是否能够解签判断是否被伪造
try:
_payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
except jwt.PyJWTError:
print('token解析失败')
return False
- 最后验证签名部分解签后的payload和payload部分的内容是否相等判断jwt是否被篡改
return payload == _payload
- 如果要添加其它的键,在里面加上相应的地方就行了
- php验证jwt的示例,生成的懒得写了,我是直接调的python的flask服务
if (isset($_COOKIE['localsign'])) {
$token = $_COOKIE['localsign'];
$secret = '123';
list($header64, $payload64, $signature) = explode('.', $token);
$decodedPayload = base64_decode(strtr($payload64, '-_', '+/'));
// 计算 payload 的签名
$computedPayloadSignature = hash_hmac('sha256', $header64.'.'.$payload64, $secret, true);
$decodedSignature = base64_decode(strtr($signature, '-_', '+/'));
// 检查 payload 数据是否被篡改
if (!hash_equals($computedPayloadSignature, $decodedSignature)) {
$response = array("code" => 1, "message" => "签名被篡改", "data" => "f");
header('Content-Type: application/json');
echo json_encode($response);
exit;
}
// 解析 payload 中的 JSON 数据
$payloadData = json_decode($decodedPayload, true);
// 检查是否存在 exp 字段
if (isset($payloadData['exp'])) {
$exp = $payloadData['exp'];
// 将 alg 视为时间戳处理
$timestamp = (int)$exp; // 将 alg 字段转换为整数作为时间戳
// 在这里添加时间戳过期检查逻辑,比如判断时间戳是否大于当前时间
$currentTimestamp = time(); // 获取当前时间戳
$expirationTime = 10; // 设置过期时间为1小时(单位:秒)
if ($currentTimestamp - $timestamp > $expirationTime) {
$response = array("code" => 0, "message" => "签名已过期", "data" => "f");
header('Content-Type: application/json');
echo json_encode($response);
exit;
}
// 验证签名
$computedSignature = hash_hmac('sha256', $header64.'.'.$payload64, $secret, true);
if (hash_equals($computedSignature, $decodedSignature)) {
echo 1;
} else {
$response = array("code" => 0, "message" => "签名错误", "data" => "f");
header('Content-Type: application/json');
echo json_encode($response);
exit;
}
} else {
$response = array("code" => 0, "message" => "签名错误", "data" => "f");
header('Content-Type: application/json');
echo json_encode($response);
exit;
}
-
注意jwt的签发和验证的字段和字段的处理方式都是必须对应的,比如上面python对于exp键的处理是存储时间戳,而下面php对于时间的验证逻辑正好也是当前的时间减去存储的时间戳是否大于设置的过期时间,类似于这样相同的处理逻辑才能一起使用
-
php的签发就省略了,上面python或者php的使用的话可以用setcookie一类的函数让客户端每次请求都带上,比如php可以这样
setcookie('miaowusign', $back, time() + 7200, '/', '', false, false);
setcookie('miaowuname', $_GET['uname'], time() + 7200, '/', '', false, false);
- cookie中成功带有jwt