JWT(JSON Web Tokens)是一种身份验证解决方式,也是 Token 的一种。

JWT 可以用于登录会话管理,在用户登录完成后,服务器可以把用户身份令牌通过 JWT 标准编码后发给客户端,客户端可以使用 Cookie、localStorage 和各种本地数据库保存。客户端在发送请求的时候只需要带上 JWT,服务器就可以通过验证 JWT 来判断用户的登录状态,不需要频繁的查询数据库验证。

因为 JWT 是保存在客户端,发送的时候也需要客户端来发送,所以 JWT 也可以用于跨域请求和不支持 Cookie 和 Session 的非浏览器客户端。

JWT 组成

JWT 使用 JSON 格式来存储数据,JWT 创建的时候包含 HeaderPayloadSignature 三个部分。

JWT 在发送到客户端的时候会转换为 base64 字符串,下面是一个完整的 JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtaXN0ZXJtYS5jb20iLCJleHAiOiIxNjk5MTcyMDIzIiwibmFtZSI6Ik1yLiBNYSJ9.8sSOIWNVhwnJkD06iIs2OoVeVNeH6arzB_4i8ny5Jbc

JWT 的 HeaderPayloadSignature 三个部分之间用点 . 分隔。

Header

Header 部分是一个 JSON,其中包含了使用的加密算法和类型。

下面是一个常见的 Header

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

alg 就是使用的加密算法,typ 是 Token 的类型。

上面的 JSON Header 最后会被转为 base64 字符串。

Payload

Payload 也是一个 JSON,里面包含要传递的数据,下面是一个简单的 Payload

{
  "exp": "1699172023",
  "name": "Mr. Ma",
  "iss": "misterma.com",
  "admin": true
}

官方也给出了一组可选的 Payload 声明建议,其中包含:

  • iss:发行者
  • exp:到期时间
  • sub:主题
  • aud:受众

上面只是官方给出的建议,你也可以自己定义需要的数据。

Payload 最后也会被转为 base64 字符串。

Signature

Signature 是根据 HeaderPayload + 私钥经过 HMAC SHA-256 之类的加密算法加密后生成的一组签名。

Signature 的主要作用是防止 JWT 的数据被篡改,如果 JWT 的数据发生改动,Signature 就无法通过验证。

生成 JWT

生成 JWT 的过程就是把 JSON 格式的 HeaderPayload 转换为 base64 字符串,然后根据 base64 的 HeaderPayload使用 HMAC SHA-256 之类的加密算法生成签名,然后把签名转换为 base64 字符串,把 base64 的 HeaderPayloadSignature. 连接起来发给客户端。

下面使用 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;

下面是详细的步骤说明:

  1. 定义 HeaderPayload 数据
  2. 使用 json_encodeHeaderPayload 转换为 JSON 字符串
  3. 使用 base64_encodeHeaderPayload 转换为 base64
  4. 使用 str_replace 把 base64 Header 和 Payload 中的一些在 URL 中可能会出错的字符替换掉,+ 替换为 -/ 替换为 _= 替换为空
  5. 定义一个私钥,这个私钥用于生成签名和验证 JWT,不会发送到客户端
  6. 使用 hash_hmac 生成签名,第一个参数是加密算法,第二个参数是 base64 的 HeaderPayloadHeaderPayload 需要用点 . 连接起来,第三个参数是 Signature 签名,第四个参数是返回二进制数据
  7. 使用 base64_encodeSignature 签名转换为 base64
  8. 使用 str_replace 把 base64 Signature 签名中一些在 URL 中可能会出错的字符替换掉,替换的规则可以看上面的 4
  9. 把 base64 的 HeaderPayloadSignature 三个部分拼接为一个字符串,三个部分之间用点 . 分隔

上面就是 PHP 生成 JWT 的简单实现。

解析和验证 JWT

解析 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 部分,根据 headeralg 来选择生成签名的算法。

下面是详细的步骤说明:

  1. 使用 explode 把 JWT 字符串拆分为数组,JWT 的分隔符是 .,拆分的时候也是用 .
  2. 使用 str_replaceSignature 签名中可能包含的 +/ 替换回来
  3. 使用 base64_decodeSignature 签名进行 base64 解码
  4. 和生成 JWT 时一样的使用 hash_hmac 生成签名,第一个参数是加密算法,第二个参数是 HeaderPayloadHeaderPayload 需要用 . 作为分隔符连接起来,第三个参数就是私钥
  5. 使用 hash_equals 对比 JWT 里的签名和刚生成的签名是否一致,如果不一致就说明 JWT 被篡改过,无法通过验证
  6. 使用 base64_decodeHeaderPayload 进行 base64 解码
  7. 使用 json_decodeHeaderPayload 转换为对象

上面已经还原了 JWT 的 HeaderPayload,你还可以根据 Payload 存储的过期时间来判断 JWT 是否过期,也可以取用 Payload 保存的数据。