使用 PHP 實作 JWT 的編碼與解碼

簡介

JSON Web Token(JWT)是一種輕量級、基於 JSON 格式的開放標準(RFC 7519),常用於身份驗證與資訊交換。JWT 的特點是不需要在伺服器端儲存使用者狀態,它包含了一段被簽章過的資料,可讓接收端驗證其完整性與真實性。

結構

JWT 結構分為三個部分:

  • Header(標頭):定義簽章使用的演算法與類型。
  • Payload(內容):實際的資料內容,例如使用者 ID、到期時間等。
  • Signature(簽章):用來驗證資料未被竄改。

格式為:

1
{Base64UrlEncode(header)}.{Base64UrlEncode(payload)}.{Base64UrlEncode(signature)}

安全性

JWT(JSON Web Token)之所以具備安全性,主要基於以下兩個核心原理:

  • 資料完整性驗證(Integrity Verification):JWT 使用 HMAC 或非對稱加密(如 RSA、ECDSA)對內容進行簽章,確保資料在傳輸過程中未被竄改。任何試圖修改 payload 的行為,都會導致簽章驗證失敗,進而無法解碼成功。
  • 不可逆簽章(Non-reversible Signature):以 HMAC 為例,簽章是根據密鑰與資料內容計算出的雜湊值。即使攻擊者能看到 JWT,也無法從簽章反推出密鑰或偽造新的簽章,除非已知密鑰。

簡言之,JWT 的安全性建立在加密雜湊函式(HMAC-SHA)或公開金鑰加密(如 RS256)所提供的資料完整性與密鑰保密性上。若搭配 HTTPS 傳輸協定,能有效避免中間人竊聽,達到足夠的傳輸與身份驗證安全性。

實作

建立專案。

1
2
mkdir php-jwt-example
cd php-jwt-example

建立 .gitignore 檔。

1
vendor

初始化專案。

1
composer init

修改 composer.json 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "memochou1993/simple-jwt",
"description": "A simple JWT implementation.",
"type": "project",
"require": {
"php": ">=7.2"
},
"autoload": {
"psr-4": {
"SimpleJWT\\": "SimpleJWT/"
}
}
}

執行安裝指令。

1
composer install

建立 SimpleJWT 資料夾。

1
mkdir SimpleJWT

SimpleJWT 資料夾,新增 JWTException.php 檔。

1
2
3
4
5
6
<?php
namespace SimpleJWT;

class JWTException extends \Exception
{
}

SimpleJWT 資料夾,新增 JWTException.php 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<?php
namespace SimpleJWT;

class JWT
{
// 預設使用的加密演算法
private static $alg = 'HS256';

// 支援的演算法對照表
private static $supported_algs = [
'HS256' => 'sha256',
];

/**
* 將資料進行 Base64 URL 安全編碼
* +、/ 會分別被替換成 -、_
* = 號會被去除以符合 URL 傳遞需求
*/
public static function base64UrlEncode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

/**
* 將 Base64 URL 編碼還原成原始資料
* 為避免 padding 不足,需補上等號 (=)
*/
public static function base64UrlDecode($data)
{
$remainder = strlen($data) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$data .= str_repeat('=', $padlen);
}
return base64_decode(strtr($data, '-_', '+/'));
}

/**
* 編碼函式:根據 payload 和密鑰產出 JWT 字串
*
* @param array $payload 資料內容,如 user_id、exp
* @param string $secret 加密密鑰
* @return string JWT 字串
*/
public static function encode(array $payload, string $secret): string
{
// 建立 JWT 標頭,指定演算法與類型
$header = ['alg' => self::$alg, 'typ' => 'JWT'];

// 編碼 header 與 payload 成 JSON,再做 Base64 URL 編碼
$headerEncoded = self::base64UrlEncode(json_encode($header));
$payloadEncoded = self::base64UrlEncode(json_encode($payload));

// 使用指定演算法進行 HMAC 簽章
$signature = hash_hmac(
self::$supported_algs[self::$alg],
"$headerEncoded.$payloadEncoded",
$secret,
true // 輸出為 raw binary
);

// 對簽章進行 Base64 URL 編碼
$signatureEncoded = self::base64UrlEncode($signature);

// 合併成完整 JWT 字串
return "$headerEncoded.$payloadEncoded.$signatureEncoded";
}

/**
* 解碼函式:根據 JWT 字串和密鑰進行驗證
*
* @param string $jwt 輸入的 JWT 字串
* @param string $secret 簽章密鑰
* @return array 解碼後的 payload
* @throws JWTException 若驗證失敗
*/
public static function decode(string $jwt, string $secret): array
{
// 切割 JWT 為三段:header.payload.signature
$parts = explode('.', $jwt);
if (count($parts) !== 3) {
throw new JWTException('Invalid segment count.');
}

// 解構三段
[$headerEncoded, $payloadEncoded, $signatureEncoded] = $parts;

// 解碼並轉回陣列
$header = json_decode(self::base64UrlDecode($headerEncoded), true);
$payload = json_decode(self::base64UrlDecode($payloadEncoded), true);
$signature = self::base64UrlDecode($signatureEncoded);

// 檢查解碼是否成功
if (!$header || !$payload) {
throw new JWTException('Invalid header or payload encoding.');
}

// 檢查演算法是否合法
if (!isset($header['alg']) || !isset(self::$supported_algs[$header['alg']])) {
throw new JWTException('Unsupported algorithm.');
}

// 計算預期的簽章
$expected_signature = hash_hmac(
self::$supported_algs[$header['alg']],
"$headerEncoded.$payloadEncoded",
$secret,
true
);

// 使用 hash_equals 防止時間攻擊
if (!hash_equals($expected_signature, $signature)) {
throw new JWTException('Invalid signature.');
}

// 若有設置過期時間,進行時間驗證
if (isset($payload['exp']) && time() > $payload['exp']) {
throw new JWTException('Expired token.');
}

// 返回解碼後的 payload
return $payload;
}
}

使用

建立 index.php 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
require_once 'vendor/autoload.php';

use SimpleJWT\JWT;
use SimpleJWT\JWTException;

/**
* JWT 範例
*
* 可以使用以下指令,建立一個隨機的 256-bit 密鑰
* php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"
*/
$secret = 'my-256-bit-secret';

// 設定期限
$ttl = 3600;

// 設定 payload 資料內容
$payload = [
'sub' => 123,
'exp' => time() + $ttl,
'iat' => time(),
'role' => 'admin',
];

// 編碼
$token = JWT::encode($payload, $secret);

echo "JWT: " . $token . PHP_EOL;

// 解碼並驗證
try {
$decoded = JWT::decode($token, $secret);
print_r($decoded);
} catch (JWTException $e) {
echo "Error: " . $e->getMessage();
}

執行程式。

1
php index.php

輸出如下:

1
2
3
4
5
6
7
8
JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEyMywiZXhwIjoxNzQ0NTc1MTgxLCJpYXQiOjE3NDQ1NzE1ODEsInJvbGUiOiJhZG1pbiJ9.yXQlSVLIAlf7WBh2dmv9pv4VbNUDKyurkwtN-i1Tppw
Array
(
[sub] => 123
[exp] => 1744575181
[iat] => 1744571581
[role] => admin
)

程式碼