什麼是 jwt ?
JWT 全稱叫 JSON Web Token, 是一個非常輕巧的規範。這個規範允許我們使用 JWT 在使用者和伺服器之間傳遞安全可靠的資訊。
jwt 使用場景
jwt
用圖廣泛,例如授權
、鑑權
等。具體一點的話,假如我們有一個 A 使用者想要邀請某使用者進入自己的群組,此時 A 使用者需要生成一條邀請連結,連結內容大致如下: https://host/group/{group_id}/invite/{invite_user}
此時這個連結點選進去雖然可以實現讓使用者加入群組,但是使用者可以隨意更改這個連結的引數,例如改改 group 後面的ID,從而加入其他任意群組,改改 invite 後面的邀請人等等操作。所以這種 URL 並不是安全的,那麼這種情況下,我們就可以使用 jwt
來實建立一個安全的邀請連結了。
首先 URL 要簡單改一下, https://host/group/invite/{token}
可以看到我們去掉了 groupId 和 inviteUser 引數,新增了一個 token
引數,可想而知, groupId 和 inviteUser 應該是被包含進 token
裡面了,如何實現這個看似很神奇的 token 呢? 我們來看看 jwt 的原理吧。
jwt 的組成、原理及實現
在講 jwt 原理之前得先知道 jwt 由哪些東西組成。
jwt 組成
一個 JWT 實際上就是一個字串,它由三部分組成,頭部、載荷與簽名。
頭部 (Header)
JWT 需要一個頭部,用於描述關於該 JWT 的最基本的資訊,例如其型別以及簽名所用的演算法等。這也可以被表示成一個 JSON 物件,如:
{
"typ": "JWT",
"alg": "md5"
}
將上面的 json 字串使用 base64 進行編碼後,可以得到一下內容,我們稱其為 JWT 的頭部(Header)。
eyJ0eXAiOiJqd3QiLCJhbGciOiJtZDUifQ==
載荷(Payload)
我們先將上面的邀請入群的操作描述成一個 JSON 物件。其中新增了一些其他的資訊,幫助今後收到這個 JWT 的伺服器理解這個JWT。
{
"sub": "1",
"iss": "http://host/group/invite",
"iat": 1451888119,
"exp": 1454516119,
"nbf": 1451888119,
"jti": "37c107e4609ddbcc9c096ea5ee76c667",
"group_id": 1,
"invite_user": "A"
}
這裡面的前6個欄位都是由JWT的標準所定義的。
- sub: 該 JWT 所面向的使用者
- iss: 該 JWT 的簽發者
- iat(issued at): 在什麼時候簽發的 token
- exp(expires): token 什麼時候過期
- nbf(not before):token 在此時間之前不能被接收處理
- jti:JWT ID為web token 提供唯一標識
將上面的 json 字串使用 base64 進行編碼後,可以得到一下內容,我們稱其為 JWT 的載荷(Payload)。
eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvOiIsImV4cCI6MTUyNzY2NzY2MywiaWF0IjoxNTI3NjY0MDYzLCJuYmYiOjE1Mjc2NjQwNjMsImdyb3VwX2lkIjoxLCJpbnZpdGVfdXNlciI6IkEiLCJqdGkiOiJlMjE4ZTJhZDdlYTdmZjUzYTVhM2RlZjA0MmFjMjM4NCJ9
簽名(Signature)
在簽名之前我們需要先得到用於簽名的字串, 將頭部和載荷使用 .
進行拼接(頭部在前), 得到用於簽名的字串
eyJ0eXAiOiJqd3QiLCJhbGciOiJtZDUifQ==.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvOiIsImV4cCI6MTUyNzY2NzY2MywiaWF0IjoxNTI3NjY0MDYzLCJuYmYiOjE1Mjc2NjQwNjMsImdyb3VwX2lkIjoxLCJpbnZpdGVfdXNlciI6IkEiLCJqdGkiOiJlMjE4ZTJhZDdlYTdmZjUzYTVhM2RlZjA0MmFjMjM4NCJ9
然後使用簽名方法對用於簽名的字串進行簽名, 得到如下字串,即 簽名(Signature)
NDljMzljOTkyOGNmYWU1NGEyZDYzMTk5NTNlNGEwZDA=
最後把用於簽名的字串和簽名使用 .
進行拼接(簽名在後), 即可得到 一個完整的 token
。但是,此時的 token
沒有帶上籤發者特有的標誌,是可以被偽造的,至於如何解決這個問題我們下面 jwt 具體實現會講。
jwt 原理
jwt 如何保證安全 ?
上面說完 jwt 組成,相信你已經知道 jwt 大概是個啥子東西了 — 就是一個字串!!!
那麼這個字串如何保證不被篡改呢 ? 這裡就要引入 secret
了。
回到上面的例子,邀請使用者入群這個場景,雖然我們上面把 引數改成了 token 這種形式,但是你可能會發現,這樣的 token 別人捕獲了之後,任然可以自己偽造一個類似的 token ,因為此時的簽名(Signature)
並沒有簽發者特有的身份資訊,所有資料都是明文的,所以這樣簽名是不安全的,應該加上 secret
進行簽名。
簽發者需要準備一個可以確認自己身份的字串,這個字串我們稱之為 secret
。以 md5
作為簽名方法為例(並不建議使用 md5 作為簽名方法)
,我們只需要將上面準備的 用於簽名的字串簡單的與 secret
進行拼接,然後進行 md5 計算,這時候得到的簽名是受 secret
值影響的,所以即便他人捕獲了之後 token
,他仍然不能隨意篡改 token 的內容,因為他不知道 secret
和拼接方法,故此時的 token
是安全的,不可被惡意篡改的。
$signatureString = `pen`; // 原始資料
$secret = `apple`; // 簽發者 secret
$originSignature = md5($signatureString .`-`. $secret);
print_r($signature); // apple-pen
$signatureString = `pen`; // 原始資料
$secret = `pineapple`; // 不一樣的 secret
$fakeSignature = md5($signatureString .`-`. $secret);
print_r($signature); // pineapple-pen
// 可以看到不一樣的 secret 會生成完全不一樣的簽名,這樣我們的資料就可以保證不能被隨意篡改了~
jwt 傳輸的資料會洩露 ?
是的,jwt 的頭部和載荷欄位都可以被解碼(base64 屬於編碼,是可以被解碼的)
。所以並不建議用 jwt 傳輸敏感資訊,例如密碼,因為這很容易被捕獲後解碼,從而被竊取。
secret 一個字串不足以描述簽發者資訊 ?
我們可以將 簽發者資訊描述成一個 json ,然後對這個 json 字串進行編碼,這樣同樣可以得到一個 secret 字串。
jwt 實現
先來一個最粗暴的 jwt 實現
最簡單粗暴的 jwt for php
實現
class JWT
{
protected $headers;
protected $payload;
/**
* @return array
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* @return array
*/
public function getPayload(): array
{
return $this->payload;
}
public function __construct(array $headers, array $payload)
{
$this->setHeaders($headers);
$this->setPayload($payload);
}
public function setHeaders(array $headers): void
{
$this->headers = $headers;
}
public function setPayload(array $payload): void
{
$this->payload = $payload;
}
/**
* 獲取用於簽名的字串
*
* @return string
*/
public function signatureStr(): string
{
$headersStr = $this::encodeStr(json_encode($this->headers));
$payloadStr = $this::encodeStr(json_encode($this->payload));
return "{$headersStr}.{$payloadStr}";
}
/**
* 編碼
*
* @param string $string
*
* @return string
*/
protected static function encodeStr(string $string): string
{
return rtrim(strtr(base64_encode($string), `+/`, `-_`), `=`);
}
/**
* 解碼
*
* @param string $string
*
* @return string
*/
protected static function decodeStr(string $string): string
{
return base64_decode(strtr($string, `-_`, `+/`));
}
/**
* 簽名,此時的 secret 為 qbhy
*
* @param string $string
*
* @return string
*/
protected static function signature(string $string): string
{
return md5($string . `qbhy`);
}
/**
* 校驗簽名
*
* @param string $signStr
* @param string $sign
*
* @return bool
*/
protected static function checkSignature(string $signStr, string $sign): bool
{
return static::signature($signStr) === $sign;
}
/**
* 生成 token
*
* @return string
*/
public function token(): string
{
$signStr = $this->signatureStr();
$token = $signStr . `.` . $this::signature($signStr);
return $token;
}
/**
* 從 token 中獲取資料
*
* @param string $token
*
* @return AppModulesJWTJWT
* @throws AppModulesJWTJWTException
*/
public static function fromToken(string $token): JWT
{
$arr = explode(`.`, $token);
if (count($arr) !== 3) {
throw new JWTException(`token 錯誤`);
}
if (!static::checkSignature("{$arr[0]}.{$arr[1]}", $arr[2])) {
throw new JWTException(`簽名錯誤`);
}
$headers = json_decode(static::decodeStr($arr[0]), true);
$payload = json_decode(static::decodeStr($arr[1]), true);
return new static($headers, $payload);
}
}
simple-jwt
這裡先安利一下我寫的一個基於 php 的 jwt 擴充套件包 — 96qbhy/simple-jwt
, 這個包實現了完整的 jwt 規範,開箱即用,你可以基於 96qbhy/simple-jwt
來給你的應用新增 jwt 相關功能。
我把 simple-jwt 拆分成,Encoder(編碼器)
、Encrypter(簽名器)
、JWT
、JWTManager
四部分,你可以自行擴充套件 Encoder
、Encrypter
,從而實現自己的編碼和加密方法,感興趣的同學可以去 github
看看原始碼 96qbhy/simple-jwt 。有問題歡迎與我討論,同時歡迎 Issue
和 PR
。
如有錯誤歡迎指出,謝謝。