写点什么

【实践】手把手带你实现 JWT 登录鉴权

作者:迷彩
  • 2022 年 8 月 30 日
    广东
  • 本文字数:6422 字

    阅读完需:约 21 分钟

前言

何为 JWT 呢?

JWT 的全称是 JSON Web Token,他是一种基于 JSON 的用于在网络上声明某种主张的令牌(token)。JWT 通常由三部分组成: 头信息(header), 载荷(payload):也就是消息体和签名(signature);他是一种用于身份提供者和服务提供者双方之间传递安全信息简洁的、URL 安全的表述性声明规范。是一个为分布式应用环境间传递身份信息而执行的一种基于 JSON 的开放标准(RFC 7519),他定义了一种简洁的,自包含的方法用于通信双方之间以 json 对象的形式安全地传递信息。因为有数字签名的存在,这些信息是可信的,JWT 可以使用 HMAC 算法或 RSA 的公私秘钥对其进行签名。

JWT 的原则是在服务器身份验证之后返回给用户的 JSON 对象如下所示:

{

"UserName": "micai",

"Role": "Admin",

"Expire": "2022-08-30 22:15:56"

}


RFC 7519 对 JWT 做的较为正式的定义如下:

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. ——JSON Web Token (JWT)


JWT 的特点

简洁(Compact): 可以通过 URL,POST 参数或者在 HTTP header 发送,因为数据量小,传输速度也很快

自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库

JWT 有何用处,解决了什么问题?

了解 jwt 的作用前,必须得了解 jwt 出现前,使用的 session 认证证模式;众所周知,http 协议本身是一种无状态的协议,当用户向我们的服务器提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再次进行用户认证才行,因为根据 http 协议,服务器并不能知道是哪个用户发出的请求,所以为了让我们的服务器中的应用能识别是哪个用户发过来的请求,我们只能在服务器中存储一份用户的登录信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时把客户端保存的 cookie 发送给我们的服务器,这样我们的服务器就能够识别这个请求到底是来自哪个用户的,这就是传统的基于 session 认证。session 具体的请求过程如下:

1.用户向服务器发送用户名和密码,服务器进行验证

2.验证通过后,相关数据(如用户角色,登录时间等)将保存在当前会话中

3.服务器会向用户返回 session_id,客户端(浏览器)会把 session 信息写入本地的 Cookie

4.用户的后续的每次请求都会从 Cookie 中取出 session_id 传给服务器

5.服务器收到 session_id 并对比之前保存的数据,确认用户的身份


但是这种基于 session 的认证使应用本身很难得到扩展,随着不同客户端用户的增加,服务器的开销增加,独立的服务器已无法承载更多的用户,这时候基于 session 认证的问题就会暴露出来,而且当应用扩展到分布式架构时需要解决 session 共享的问题,又因为是基于 cookie 来进行用户识别的, cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击.


这时候就该 JWT 上场拯救了,JWT 是目前最流行的跨域身份验证解决方案,基于 token 的鉴权机制类似于 http 协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息,一旦用户完成了登陆,在接下来的每个请求中都会包含 JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限的验证.由于它的开销非常小,可以轻松的在不同域名的系统中传递,这也为应用的扩展提供了便利.目前在单点登录(SSO)中应用比较广泛.


大致流程上是这样的:

  1. 用户使用用户名密码来请求服务器

  2. 服务器进行验证用户的信息

  3. 服务器通过验证发送给用户一个 token

  4. 客户端存储 token,并在每次请求时附送上这个 token

  5. 服务端验证 token,并返回数据


这个 token 必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *


JWT 的构成

jwt 大概长下面这样:这里使用的是 HS256 进行加密.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iui_t-W9qSIsImlhdCI6MTY2MTg3MTk3OX0.fa9akf1yhTjH_ovdW5Q0kuY8yDmaItpBGKABKKWFp5Q
复制代码

红色部分为 JWT 头,紫色部分为有效载荷,蓝色部分为签名

下面截图中的结果是使用https://jwt.io进行解析的:


JWT 头(header)

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

{  "alg": "HS256",  "typ": "JWT"}
复制代码

在上面的代码中,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为 HS256);typ 属性表示令牌的类型,JWT 令牌统一写为 JWT

最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存。

有效载荷(payload)

有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT 指定七个默认字段供选择:

iss:发行人

exp:到期时间

sub:主题

aud:用户

nbf:在此之前不可用

iat:发布时间

jti:JWT ID 用于标识该 JWT

{  "sub": "1234567890",  "name": "迷彩",  "iat": 1661871979}
复制代码

除以上默认字段外,我们还可以自定义私有字段,如下例:

{  "sub": "1234567890",  "name": "迷彩",  "status": true}
复制代码

请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

JSON 对象也使用 Base64 URL 算法转换为字符串保存

签名哈希(signature)

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),

secret)

如下图:


在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个 JWT 对象。

Base64URL 算法

作为令牌的 JWT 可以放在 URL 中(例如 api.micai/?token=xxxxxx). Base64 中用的三个字符是"+","/"和"=",由于在 URL 中有特殊含义,因此 Base64URL 中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是 Base64URL 算法

JWT 认证流程

综上所述.jwt 鉴权验证的流程可总结为以下几点:

  1. 在 JWT 头部信息中声明加密算法和常量, 然后把 header 使用 json 转化为字符串

  2. 在载荷中声明用户信息,同时还有一些其他的内容.再次使用 json 把载荷部分进行转化,转化为字符串

  3. 使用在 header 中声明的加密算法和每个项目随机生成的 secret 来进行加密.把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串.这个字符串是独一无二的

  4. 解密的时候,只要客户端带着 JWT 来发起请求,服务端就直接使用 secret 进行解密

:解密的时候没有使用数据库,仅仅使用的是 secret 进行解密,所以 JWT 的 secret 一定要保管好!

JWT 登录鉴权的实现

这里会使用到一个 jwt 相关的库:lcobucci/jwt,该库的地址为:https://github.com/lcobucci/jwt

lcobucci/jwt 是一个与框架无关的 PHP 库,允许您基于RFC 7519发布、解析和验证 JSON Web 令牌

如果使用composer require lcobucci/jwt安装最新版本有如下问题可降低版本安装

我这里安装的是 3.0 的版本

composer require lcobucci/jwt 3.*

安装完成之后你在后台就可以看到 composer.json 多了lcobucci

vendor 文件下也会多了lcobucci的文件夹


这里使用了 composer 进行包管理,好处就是不管我们需要加载多少第三库,我们只需要使用require 'vendor/autoload.php';进行加载即可,实际使用到的库 composer 会帮我们自动加载


我们接下来看看具体实现:

如果你在使用 lcobucci 时出现以下告警提示,那是因为你使用的版本中已经启用了相关的用法,你可以使用新的方式,或者将 lcobucci 的版本降低到 3.3 或以下

本文使用的是 3.4.6 版本.

代码实现

相关类的使用说明:

Lcobucci\JWT\Validation\Constraint\IdentifiedBy: 验证jwt id是否匹配Lcobucci\JWT\Validation\Constraint\IssuedBy: 验证签发人参数是否匹配Lcobucci\JWT\Validation\Constraint\PermittedFor: 验证受众人参数是否匹配Lcobucci\JWT\Validation\Constraint\RelatedTo: 验证自定义cliam参数是否匹配Lcobucci\JWT\Validation\Constraint\SignedWith: 验证令牌是否已使用预期的签名者和密钥签名Lcobucci\JWT\Validation\Constraint\StrictValidAt: ::验证存在及其有效性的权利要求中的iat,nbf和exp(支持余地配置Lcobucci\JWT\Validation\Constraint\LooseValidAt: 验证的权利要求iat,nbf和exp,当存在时(支持余地配置)
复制代码

在项目根目录新建 testjwt.php 文件,引入的相关的类

<?phpnamespace Wmtest;error_reporting(0);//屏蔽提示require 'vendor/autoload.php';//引入自动加载use Lcobucci\JWT\Configuration;use Lcobucci\JWT\Parser;use Lcobucci\JWT\Signer\Hmac\Sha256;use Lcobucci\JWT\Signer\Key\InMemory;use Lcobucci\JWT\Token\Plain;use Lcobucci\JWT\Validation\Constraint\IdentifiedBy;use Lcobucci\JWT\Validation\Constraint\IssuedBy;use Lcobucci\JWT\Validation\Constraint\PermittedFor;use Lcobucci\JWT\ValidationData;
复制代码


创建 Token 类,并实现 token 的生成和验证功能

<?phpclass Token{    protected $issuer = "http://micai.com";    protected $audience = "http://micai.io";    protected $id = "5t6y9400453";    // jwt_sercetkey一定要保管好不要泄露,这里使用随机密码生成器搞一个https://suijimimashengcheng.bmcx.com/    protected static $jwt_sercetkey = "1hKopf4l9FqCRxmw6D1KXSN8fei8EbFsTL73Kja2pQmwv3Xv";
/** * 签发token */ public function getToken() { $time = time(); $config = self::getConfig(); assert($config instanceof Configuration);
/* iss 【issuer】签发人(可以是,发布者的url地址)
sub 【subject】该JWT所面向的用户,用于处理特定应用,不是常用的字段
aud 【audience】受众人(可以是客户端的url地址,用作验证是否是指定的人或者url)
exp 【expiration】 该jwt销毁的时间;unix时间戳
nbf 【not before】 该jwt的使用时间不能早于该时间;unix时间戳
iat 【issued at】 该jwt的发布时间;unix 时间戳
jti 【JWT ID】 该jwt的唯一ID编号 */ $token = $config->builder() ->issuedBy($this->issuer) // iss 【issuer】签发人(可以是,发布者的url地址) ->permittedFor($this->audience) // aud 【audience】受众人(可以是客户端的url地址,用作验证是否是指定的人或者url) ->identifiedBy($this->id, true) // jti 【JWT ID】 该jwt的唯一ID编号 ->issuedAt($time) // iat 【issued at】 该jwt的发布时间;unix 时间戳 ->canOnlyBeUsedAfter($time + 1) // nbf 【not before】 该jwt的使用时间不能早于该时间;unix时间戳即生效时间 ->expiresAt($time + 3600) // exp 【expiration】 该jwt销毁的时间;unix时间戳 ->withClaim('uid', 999) // 用户id ->withClaim('username', "Micai") // 用户名 ->getToken($config->signer(), $config->signingKey()); // 生成签名 return $token->toString(); //返回签名字符串 }
/** * 验证 jwt token 并返回其中的用户id */ public function verifyToken_other($token) { try { $config = self::getConfig(); //获取配置 assert($config instanceof Configuration);
$token = $config->parser()->parse($token);//解析 assert($token instanceof Plain); //验证jwt id是否匹配 $validate_jwt_id = new IdentifiedBy($this->id); //验证签发人url是否正确 $validate_issued = new IssuedBy($this->issuer); //验证客户端url是否匹配 $validate_aud = new PermittedFor($this->audience); $config->setValidationConstraints($validate_jwt_id, $validate_issued, $validate_aud);
$constraints = $config->validationConstraints();
if (!$config->validator()->validate($token, ...$constraints)) { die("无效token!"); } } catch (\Exception $e) { die("错误:" . $e->getMessage()); } $jwtContent = $token->claims(); // 这是jwt token中存储的所有信息 print_r($jwtContent); return json_encode(array('uid'=>$jwtContent->get("uid"),'username'=>$jwtContent->get("username")),JSON_UNESCAPED_UNICODE); // 获取uid和username }
/** * 加密解密使用的配置 * @return Configuration */ public static function getConfig() { $configuration = Configuration::forSymmetricSigner( new Sha256(), //除了256你还可以使用384,512的加密算法 InMemory::base64Encoded(self::$jwt_sercetkey) //这里可重写 ); return $configuration; }
/** * 另一种验证方法,但是已经弃用 * verify token */ public function verifyToken($token) { $token = (new Parser())->parse((string)$token); //验证token $data = new ValidationData(); $data->setIssuer($this->issuer);//验证的签发人 $data->setAudience($this->audience);//验证的接收人 $data->setId($this->id);//验证token标识
if (!$token->validate($data)) { //token验证失败 die("无效token!"); }
$jwtContent = $token->claims(); // 这是jwt token中存储的所有信息 print_r($jwtContent); return json_encode(array('uid'=>$jwtContent->get("uid"),'username'=>$jwtContent->get("username")),JSON_UNESCAPED_UNICODE); // 获取uid和username }}
复制代码

验证 Token 类

<?phpnamespace Wmtest;require 'testjwt.php'; $new = new Token();$token = $new->getToken();echo '获取的token如下:<br>';print_r($token);?>
复制代码

执行结果如下:

获取的token如下:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjV0Nnk5NDAwNDUzIn0.eyJpc3MiOiJodHRwOlwvXC9taWNhaS5jb20iLCJhdWQiOiJodHRwOlwvXC9taWNhaS5pbyIsImp0aSI6IjV0Nnk5NDAwNDUzIiwiaWF0IjoxNjYxODgxNjkxLCJuYmYiOjE2NjE4ODE2OTIsImV4cCI6MTY2MTg4NTI5MSwidWlkIjo5OTksInVzZXJuYW1lIjoiTWljYWkifQ.qUupkPdNKlK3n7bQIYjpKJfSbro0gW6RxPHWEf9U7EI
复制代码

使用上面生成的 token,获取用户信息

<?phpnamespace Wmtest;require 'testjwt.php';$token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjV0Nnk5NDAwNDUzIn0.eyJpc3MiOiJodHRwOlwvXC9taWNhaS5jb20iLCJhdWQiOiJodHRwOlwvXC9taWNhaS5pbyIsImp0aSI6IjV0Nnk5NDAwNDUzIiwiaWF0IjoxNjYxODgxNjkxLCJuYmYiOjE2NjE4ODE2OTIsImV4cCI6MTY2MTg4NTI5MSwidWlkIjo5OTksInVzZXJuYW1lIjoiTWljYWkifQ.qUupkPdNKlK3n7bQIYjpKJfSbro0gW6RxPHWEf9U7EI';$new = new Token();echo '验证token结果如下:<br>';$arr = $new->verifyToken_other($token);print_r("<br>获取用户id和用户名:".$arr);
复制代码

执行结果如下:


获取用户id和用户名:{"uid":999,"username":"Micai"}
复制代码


发布于: 2022 年 08 月 30 日阅读数: 59
用户头像

迷彩

关注

我的工作是常年写bug|公众号:编程架构之美 2020.06.18 加入

修bug的菜鸟~公众号:“互联网有啥事”已改名为“编程架构之美”

评论

发布
暂无评论
【实践】手把手带你实现JWT登录鉴权_分布式_迷彩_InfoQ写作社区