资讯专栏INFORMATION COLUMN

关于 jwt 你应该知道的事情

leone / 3042人阅读

摘要:头部需要一个头部,用于描述关于该的最基本的信息,例如其类型以及签名所用的算法等。签发者需要准备一个可以确认自己身份的字符串,这个字符串我们称之为。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/28719.html

相关文章

  • 前端应该知道web登录

    摘要:客户端发起非登录请求时,服务端通过中的找到对应的来知道此次请求是谁发出的。数量随着登录用户的增多而增多,存储会增加很多。还记得在上家公司做全干工程师的时候,基本从页面写到运维,当时做登录这块的时候,被session、cookie、token各种概念差点整蒙圈了,上网查询相关概念,发现很多人都是类似的疑惑,比如:showImg(https://user-gold-cdn.xitu.io/201...

    NervosNetwork 评论0 收藏0
  • 弄懂JWT原理

    摘要:的组成一个实际上就是一个字符串,它由三部分组成,头部载荷与签名。这个字符串我们将它称作的载荷。注意是一种编码,它是可以被翻译回原来的样子来的。这也可以被表示成一个对象。 JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。 让我们来假想一下一个场景。在A用户关注了B用户的时候,系统发邮件给B用户,并且附有一个链接点...

    animabear 评论0 收藏0
  • 编写 Node.js Rest API 10 个最佳实践

    摘要:要对进行黑盒测试测试的最好办法是对他们进行黑盒测试,黑盒测试是一种不关心应用内部结构和工作原理的测试方法,测试时系统任何部分都不应该被。此外,有了黑盒测试并不意味着不需要单元测试,针对的单元测试还是需要编写的。 本文首发于之乎专栏前端周刊,全文共 6953 字,读完需 8 分钟,速度需 2 分钟。翻译自:RingStack 的文章 https://blog.risingstack.co...

    ermaoL 评论0 收藏0
  • 微服务实战:从架构到发布(二)

    摘要:微服务架构着重培养通用可重用的服务。服务注册和发现微服务架构下,有大量的微服务需要处理。网关也是获得微服务状态监控信息的中心。实际情况是,微服务和其它企业架构并存。 引言:上篇文章介绍了微服务和单体架构的区别、微服务的设计、消息、服务间通信、数据去中心化,本篇会继续深入微服务,介绍其它特性。 治理去中心化 通常治理的意思是构建方案,并且迫使人们通过努力达到组织的目标。SOA治理指导开发...

    JinB 评论0 收藏0
  • 微服务实战:从架构到发布(二)

    摘要:微服务架构着重培养通用可重用的服务。服务注册和发现微服务架构下,有大量的微服务需要处理。网关也是获得微服务状态监控信息的中心。实际情况是,微服务和其它企业架构并存。 引言:上篇文章介绍了微服务和单体架构的区别、微服务的设计、消息、服务间通信、数据去中心化,本篇会继续深入微服务,介绍其它特性。 治理去中心化 通常治理的意思是构建方案,并且迫使人们通过努力达到组织的目标。SOA治理指导开发...

    zhaot 评论0 收藏0

发表评论

0条评论

leone

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<