关于 jwt ,你应该知道的
31

什么是 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(签名器)JWTJWTManager 四部分,你可以自行扩展 EncoderEncrypter,从而实现自己的编码和加密方法,感兴趣的同学可以去 github 看看源码 96qbhy/simple-jwt 。有问题欢迎与我讨论,同时欢迎 IssuePR
如有错误欢迎指出,谢谢。

桥边红药

本帖由系统于 4个月前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 12
果子小沫

:+1::+1::+1:
师兄,有个错别字。用图

4个月前
96qbhy

@果子小沫 还真有,哈哈。谢谢指出。

4个月前
medz

小小建议,代码中调用本类静态方法的调用修改推荐如下:

  • $this::encodeStr -> static::encodeStr
  • $this::decodeStr -> static::decodeStr

file

4个月前
96qbhy

@medz 谢谢建议。这里只是顺手这样写了,实际运行并不会有问题的。而且 simple-jwt 扩展包里面并没有这样写的。

4个月前
medz

@96qbhy ?

file
你又骗我!!!

4个月前
96qbhy

@medz 哈哈

4个月前
Littlesqx

第一见这样的写法:$this::,很奇怪。。。

4个月前
96qbhy

@Littlesqx 我习惯有 $this 的地方就用 $this

4个月前

不是应该用self::xxx()的格式吗?

4个月前
96qbhy

@堂堂糖唐 不一定的啊,看你习惯

4个月前
Hanson

很好,所以我选择 passport

3个月前
96qbhy

@Hanson 我也是闲着没事做而已,而且现在好像还没看到比较单纯的 jwt 包,基本都是跟 auth 功能绑定的, 其实 jwt 可以单独拎出来做好多其他事情啊

3个月前

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!