JWT(JSON Web Tokens)是一种身份验证解决方式,也是 Token 的一种。
JWT 可以用于登录会话管理,在用户登录完成后,服务器可以把用户身份令牌通过 JWT 标准编码后发给客户端,客户端可以使用 Cookie、localStorage 和各种本地数据库保存。客户端在发送请求的时候只需要带上 JWT,服务器就可以通过验证 JWT 来判断用户的登录状态,不需要频繁的查询数据库验证。
因为 JWT 是保存在客户端,发送的时候也需要客户端来发送,所以 JWT 也可以用于跨域请求和不支持 Cookie 和 Session 的非浏览器客户端。
JWT 组成 {#JWT+%E7%BB%84%E6%88%904036}
JWT 使用 JSON 格式来存储数据,JWT 创建的时候包含 Header
、Payload
、Signature
三个部分。
JWT 在发送到客户端的时候会转换为 base64 字符串,下面是一个完整的 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtaXN0ZXJtYS5jb20iLCJleHAiOiIxNjk5MTcyMDIzIiwibmFtZSI6Ik1yLiBNYSJ9.8sSOIWNVhwnJkD06iIs2OoVeVNeH6arzB_4i8ny5Jbc
JWT 的 Header
、Payload
、Signature
三个部分之间用点 .
分隔。
Header {#Header6909}
Header
部分是一个 JSON,其中包含了使用的加密算法和类型。
下面是一个常见的 Header
:
{
"alg": "HS256",
"typ": "JWT"
}
alg
就是使用的加密算法,typ
是 Token 的类型。
上面的 JSON Header
最后会被转为 base64 字符串。
Payload {#Payload1657}
Payload
也是一个 JSON,里面包含要传递的数据,下面是一个简单的 Payload
:
{
"exp": "1699172023",
"name": "Mr. Ma",
"iss": "misterma.com",
"admin": true
}
官方也给出了一组可选的 Payload
声明建议,其中包含:
iss
:发行者exp
:到期时间sub
:主题aud
:受众
上面只是官方给出的建议,你也可以自己定义需要的数据。
Payload
最后也会被转为 base64 字符串。
Signature {#Signature3735}
Signature
是根据 Header
和 Payload
+ 私钥经过 HMAC SHA-256 之类的加密算法加密后生成的一组签名。
Signature
的主要作用是防止 JWT 的数据被篡改,如果 JWT 的数据发生改动,Signature
就无法通过验证。
生成 JWT {#%E7%94%9F%E6%88%90+JWT3983}
生成 JWT 的过程就是把 JSON 格式的 Header
和 Payload
转换为 base64 字符串,然后根据 base64 的 Header
和 Payload
使用 HMAC SHA-256 之类的加密算法生成签名,然后把签名转换为 base64 字符串,把 base64 的 Header
、Payload
、Signature
用 .
连接起来发给客户端。
下面使用 PHP 生成 JWT:
$header = array('alg' => 'HS256', 'typ' => 'JWT');
$payload = array(
'iss' => 'misterma.com',
'exp' => '1699172023',
'name' => 'Mr. Ma'
);
// 把 header 转换为 JSON 字符串,然后进行 base64 编码
$header = base64_encode(json_encode($header));
// 把 payload 转换为 JSON 字符串,然后进行 base64 编码
$payload = base64_encode(json_encode($payload));
// 把一些在 URL 参数中可能会出错的字符替换掉
$header = str_replace(array('+', '/', '='), array('-', '_', ''), $header);
$payload = str_replace(array('+', '/', '='), array('-', '_', ''), $payload);
$secretKey = '123456'; // 私钥
// 使用 HMAC SHA-256 根据 header 和 payload 生成一个签名
$signature = hash_hmac('sha256', $header . '.' . $payload, $secretKey, true);
// 把 signature 签名转换为 base64
$signature = base64_encode($signature);
// 把一些在 URL 参数中可能会出错的字符替换掉
$signature = str_replace(array('+', '/', '='), array('-', '_', ''), $signature);
// 拼接 JWT
$jwtStr = $header . '.' . $payload . '.' . $signature;
// 输出 JWT
echo $jwtStr;
下面是详细的步骤说明:
- 定义
Header
和Payload
数据 - 使用
json_encode
把Header
和Payload
转换为 JSON 字符串 - 使用
base64_encode
把Header
和Payload
转换为 base64 - 使用
str_replace
把 base64 Header 和 Payload 中的一些在 URL 中可能会出错的字符替换掉,+
替换为-
,/
替换为_
,=
替换为空 - 定义一个私钥,这个私钥用于生成签名和验证 JWT,不会发送到客户端
- 使用
hash_hmac
生成签名,第一个参数是加密算法,第二个参数是 base64 的Header
和Payload
,Header
和Payload
需要用点.
连接起来,第三个参数是Signature
签名,第四个参数是返回二进制数据 - 使用
base64_encode
把Signature
签名转换为 base64 - 使用
str_replace
把 base64Signature
签名中一些在 URL 中可能会出错的字符替换掉,替换的规则可以看上面的4
- 把 base64 的
Header
、Payload
、Signature
三个部分拼接为一个字符串,三个部分之间用点.
分隔
上面就是 PHP 生成 JWT 的简单实现。
解析和验证 JWT {#%E8%A7%A3%E6%9E%90%E5%92%8C%E9%AA%8C%E8%AF%81+JWT6708}
解析 JWT 和生成是差不多的,只是解析是反过来。
客户端在发送 JWT 的时候,可能会把 JWT 放到 header
请求头发送,也可能会直接放到请求 body
里发送。header
一般是放到 Authorization
里发送,PHP 获取的方式如下:
$headers = getallheaders();
$jwt = $headers['Authorization'];
我下面只是简单演示,JWT 就直接写到 $jwt
变量里。
下面使用 PHP 解析和验证 JWT:
// JWT 字符串
$jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtaXN0ZXJtYS5jb20iLCJleHAiOiIxNjk5MTcyMDIzIiwibmFtZSI6Ik1yLiBNYSJ9.IeYKIrOm7Isx6NSwkeZ0YlL-cNL7Pqv-PLyiocn6pE4';
// 使用 . 把 JWT 字符串拆分为数组,然后把数组传给三个变量
list($base64Header, $base64Payload, $base64Signature) = explode('.', $jwt);
$secretKey = '123456'; // 私钥
// 把签名中的 + 和 / 替换回来
$base64Signature = str_replace(array('-', '_'), array('+', '/'), $base64Signature);
// 对签名进行 base64 解码
$signature = base64_decode($base64Signature);
// 使用 HMAC SHA-256 根据 header 和 payload 生成一个签名
$validSignature = hash_hmac('sha256', $base64Header . '.' . $base64Payload, $secretKey, true);
// 对比两个签名,判断 JWT 是否被篡改过
if (!hash_equals($signature, $validSignature)) {
// 如果 JWT 被篡改就不再往下执行
exit('JWT被篡改,无法通过签名验证');
}
// 如果 JWT 没有被篡改就继续还原 JWT数据
// 把 base64 header 和 payload 中的 + 和 / 替换回来
$base64Header = str_replace(array('-', '_'), array('+', '/'), $base64Header);
$base64Payload = str_replace(array('-', '_'), array('+', '/'), $base64Payload);
// 对 base64 header 和 payload 进行 base64 解码和 JSON 解码
$header = json_decode(base64_decode($base64Header));
$payload = json_decode(base64_decode($base64Payload));
// 输出 header 和 payload
print_r($header);
print_r($payload);
我上面的代码只是简单演示,签名算法也是固定的,如果你需要更通用的验证方式的话,可以先还原 header
部分,根据 header
的 alg
来选择生成签名的算法。
下面是详细的步骤说明:
- 使用
explode
把 JWT 字符串拆分为数组,JWT 的分隔符是.
,拆分的时候也是用.
- 使用
str_replace
把Signature
签名中可能包含的+
和/
替换回来 - 使用
base64_decode
对Signature
签名进行 base64 解码 - 和生成 JWT 时一样的使用
hash_hmac
生成签名,第一个参数是加密算法,第二个参数是Header
和Payload
,Header
和Payload
需要用.
作为分隔符连接起来,第三个参数就是私钥 - 使用
hash_equals
对比 JWT 里的签名和刚生成的签名是否一致,如果不一致就说明 JWT 被篡改过,无法通过验证 - 使用
base64_decode
对Header
和Payload
进行 base64 解码 - 使用
json_decode
把Header
和Payload
转换为对象
上面已经还原了 JWT 的 Header
和 Payload
,你还可以根据 Payload
存储的过期时间来判断 JWT 是否过期,也可以取用 Payload
保存的数据。