什么是 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 \App\Modules\JWT\JWT
* @throws \App\Modules\JWT\JWTException
*/
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
。
如有错误欢迎指出,谢谢。