您的位置 首页 php

使用 JWT(JSON Web Token 令牌)进行 PHP 授权

曾经有一段时间,通过应用程序验证自己的唯一方法是提供您的凭据(通常是用户名或电子邮件地址和密码),然后使用会话来维护用户状态,直到用户注销。不久之后,我们开始使用身份验证 API。最近,JWT 或 JSON Web Tokens 越来越多地被用作对服务器请求进行身份验证的另一种方式。

在本文中,您将了解 JWT 是什么以及如何将它们与 PHP 一起使用来发出经过身份验证的用户请求。

JWT 与会话

但首先, 为什么会话不是一件好事?嗯,有三个关键原因:

  • 数据以纯文本形式存储在服务器上
    即使数据通常不存储在公用文件夹中,任何对服务器具有足够访问权限的人都可以读取会话文件的内容。
  • 它们涉及文件系统读/写请求
    每次会话开始或其数据被修改时,服务器都需要更新会话文件。每次应用程序发送会话 cookie 时也是如此。如果您有大量用户,则可能会导致服务器运行缓慢,除非您使用其他会话存储选项,例如 Memcached Redis
  • 分布式/集群应用程序
    由于会话文件默认存储在文件系统上,因此很难为高可用性应用程序(需要使用负载平衡器和集群服务器等技术的应用程序)提供分布式或集群基础架构。必须实施其他存储介质和特殊配置——并且在充分意识到它们的影响的情况下这样做。

智威汤逊

现在,让我们开始学习 JWT。JSON Web Token 规范 ( RFC 7519)于 2010 年 12 月 28 日首次发布,最近一次更新于 2015 年 5 月。

与 API 密钥相比,JWT 具有许多优势,包括:

  • API 密钥是随机字符串,而 JWT 包含信息和元数据。这些信息和元数据可以描述范围广泛的事物,例如用户的身份、授权数据以及令牌在时间范围内或与域相关的有效性。
  • JWT 不需要集中的发行或撤销机构。
  • JWT 与 OAUTH2 兼容。
  • 可以检查 JWT 数据。
  • JWT 有过期控制。
  • JWT 适用于空间受限的环境,例如 HTTP 授权标头。
  • 数据以 JavaScript Object Notation 格式 ( JSON ) 传输。
  • JWT 使用Base64url 编码表示

JWT 长什么样?

这是一个示例 JWT:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E
  

乍一看, 字符串 似乎只是与句点或点字符连接的随机字符组。因此,它可能看起来与 API 密钥没有太大区别。但是,如果您仔细观察,会发现有三个单独的字符串。

JWT 标头

第一个字符串是 JWT 标头。它是一个Base64、URL 编码的 JSON 字符串。它指定了用于生成签名的加密算法,以及令牌的类型,该类型始终设置为JWT。该算法可以是 对称 的或非 对称 的。

对称算法 使用单个密钥来创建和验证令牌。密钥在 JWT 的创建者和它的消费者之间共享。您必须确保只有创建者和消费者知道秘密。否则, 任何人都 可以创建有效的令牌。

非对称算法 使用 私钥 对令牌进行签名,并使用公钥对其进行验证。当共享秘密不切实际或其他方只需要验证令牌的完整性时,应使用这些算法。

JWT 的有效负载

第二个字符串是 JWT 的有效负载。它也是一个 Base64、URL 编码的 JSON 字符串。它包含一些标准字段,称为“声明”。有三种类型的声明: 注册 的、 公共 的和 私有 的。

注册的索赔 是预定义的。您可以在JWT 的 RFC中找到它们的列表。以下是一些常用的:

  • iat :令牌发行的时间戳。
  • key :一个唯一的字符串,可用于验证令牌,但与没有集中的发行者权限相违背。
  • iss : 包含发行者名称或标识符的字符串。可以是域名,可用于丢弃来自其他应用程序的令牌。
  • nbf :令牌应该开始被认为有效的时间戳。应该等于或大于 iat
  • exp :令牌应停止有效的时间戳。应该大于 iat nbf

可以按照您认为合适的方式定义公共声明。但是,它们不能与已注册的声明或已存在的公共声明的声明相同。您可以随意创建私人索赔。它们仅用于两方之间:生产者和消费者。

JWT 的签名

JWT 的签名是一种加密机制,旨在使用令牌内容独有的数字签名来保护 JWT 的数据。签名确保 JWT 的完整性,以便消费者可以验证它没有被恶意行为者篡改。

JWT 的签名是三件事的组合:

  • JWT 的标头
  • JWT 的有效载荷
  • 一个秘密值

这三个使用 JWT 标头中指定的算法进行数字签名( 未加密)。 如果我们对上面的示例进行解码,我们将得到以下 JSON 字符串:

JWT 的标头

 {
    "alg": "HS256",
    "typ": "JWT"
}
  

JWT 的数据

 {
    "iat": 1416929109,
    "jti": "aa7f8d0a95c",
    "scopes": [
        "repo",
        "public_repo"
    ]
}
  

亲自试用jwt.io,您可以在其中对自己的 JWT 进行编码和解码。

让我们在基于 PHP 的应用程序中使用 JWT

现在您已经了解了 JWT 是什么,现在是学习如何在 PHP 应用程序中使用它们的时候了。在我们深入研究之前,请随意克隆本文的代码,或者在我们进行的过程中跟随并创建它。

有很多方法可以实现集成 JWT,但我们将采用以下方法。

除登录和注销页面外,对应用程序的所有请求都需要通过 JWT 进行身份验证。如果用户在没有 JWT 的情况下发出请求,他们将被重定向到登录页面。

用户填写并提交登录表单后,表单将通过 JavaScript 提交到authenticate.php我们应用程序中的登录端点 。然后端点将从请求中提取凭据(用户名和密码)并检查它们是否有效。

如果是,它将生成一个 JWT 并将其发送回客户端。当客户端收到 JWT 时,它将存储它并在以后对应用程序的每个请求中使用它。

对于一个简单的场景,用户只能请求一个资源——一个恰当命名为resource.php. 它不会做太多,只是返回一个字符串,其中包含请求时的当前时间戳。

发出请求时有几种使用 JWT 的方法。在我们的应用程序中,JWT 将在Bearer 授权标头中发送。

如果您不熟悉承载授权,它是一种 HTTP 身份验证形式,其中令牌(例如 JWT)在请求标头中发送。服务器可以检查令牌并确定是否应将访问权限授予令牌的“持有者”。

这是标题的示例:

 Authorization: Bearer ab0dde18155a43ee83edba4a4542b973
  

对于我们的应用程序收到的每个请求,PHP 将尝试从 Bearer 标头中提取令牌。如果存在,则对其进行验证。如果它有效,用户将看到该请求的正常响应。但是,如果 JWT 无效,则不允许用户访问该资源。

请注意,JWT 并非 旨在替代会话 cookie。

先决条件

首先,我们需要在我们的系统上安装PHP和 Composer

在项目的根目录中,运行composer install. 这将引入 Firebase PHP-JWT,这是一个第三方库,可简化 JWT 的使用,以及 laminas -config,旨在简化对应用程序中配置数据的访问

登录表格

安装完库后,让我们单步执行authenticate.php. 我们首先进行常规设置,确保 Composer 生成的自动加载器可用。

 <?php

declare(strict_types=1);

use Firebase\JWT\JWT;

require_once('../vendor/autoload.php');
  

收到表单提交后,凭据将根据数据库或其他一些数据存储进行验证。出于本示例的目的,我们将假设它们是有效的,并设置$hasValidCredentials为 true。

 <?php

// extract credentials from the  request 

if ($hasValidCredentials) {
  

接下来,我们初始化一组用于生成 JWT 的变量。请记住,由于可以在客户端检查 JWT, 因此不要 在其中包含任何敏感信息。

值得再次指出的另一件事是,它$secretKey不会像这样初始化。您可能会在环境中设置它并使用诸如phpdotenv 之类的库或在配置文件中提取它。在此示例中,我避免了这样做,因为我想专注于 JWT 代码。

永远不要泄露它或将它存储在版本控制之下!

 $secretKey  = 'bGS6lzFqvvSQ8ALbOxatm7/Vk7mLQyzqaS34Q4oR1ew=';
$issuedAt   = new DateTimeImmutable();
$expire     = $issuedAt->modify('+6 minutes')->getTimestamp();      // Add 60 seconds
$serverName = "your.domain.name";
$username   = "username";                                           // Retrieved from filtered POST data

$data = [
    'iat'  => $issuedAt->getTimestamp(),         // Issued at: time when the token was generated
    'iss'  => $serverName,                       // Issuer
    'nbf'  => $issuedAt->getTimestamp(),         // Not before
    'exp'  => $expire,                           // Expire
    'userName' => $username,                     // User name
];
  

准备好有效负载数据后,我们接下来使用 php-jwt 的静态encode方法创建 JWT。

方法:

  • 将数组转换为 JSON
  • 产生标题
  • 签署有效载荷
  • 编码最终的字符串

它需要三个参数:

  • 有效载荷信息
  • 密钥
  • 用于签署令牌的算法

通过调用echo函数的结果,返回生成的令牌:

 <?php
    // Encode the array to a JWT string.
    echo JWT::encode(
        $data,
        $secretKey,
        'HS512'
    );
}
  

使用 JWT

现在客户端有了令牌,您可以使用 JavaScript 或您喜欢的任何机制来存储它。这是一个如何使用 vanilla JavaScript 执行此操作的示例。中index.html,表单提交成功后,返回的JWT存储在内存中,登录表单隐藏,显示请求时间戳的按钮:

 const store = {};
const loginButton = document.querySelector('#frmLogin');
const btnGetResource = document.querySelector('#btnGetResource');
const form = document.forms[0];

// Inserts the jwt to the store object
store.setJWT = function (data) {
  this.JWT = data;
};

loginButton.addEventListener('submit', async (e) => {
  e.preventDefault();

  const res = await fetch('/authenticate.php', {
    method: 'POST',
    headers: {
      'Content-type': 'application/x-www-form-urlencoded;  charset =UTF-8'
    },
    body: JSON.stringify({
      username: form.inputEmail.value,
      password: form.inputPassword.value
    })
  });

  if (res.status >= 200 && res.status <= 299) {
    const jwt = await res.text();
    store.setJWT(jwt);
    frmLogin.style.display = 'none';
    btnGetResource.style.display = 'block';
  } else {
    // Handle errors
    console.log(res.status, res.statusText);
  }
});
  

使用智威汤逊

单击“获取当前时间戳”按钮时,会向 发出 GET 请求,该请求会resource.php在 Authorization 标头中设置身份验证后收到的 JWT。

 btnGetResource.addEventListener('click', async (e) => {
  const res = await fetch('/resource.php', {
    headers: {
      'Authorization': `Bearer ${store.JWT}`
    }
  });
  const timeStamp = await res.text();
  console.log(timeStamp);
});
  

当我们单击该按钮时,会发出类似于以下的请求:

 GET /resource.php HTTP/1.1
Host: yourhost.com
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttp Request 
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0
  

假设 JWT 有效,我们会看到资源,然后将响应写入控制台。

验证 JWT

最后,让我们看看如何在 PHP 中验证令牌。和往常一样,我们会包含 Composer 的自动加载器。然后,我们可以选择检查是否使用了正确的请求方法。我跳过了执行此操作的代码,继续关注 JWT 特定的代码:

 <?php
chdir(dirname(__DIR__));

require_once('../vendor/autoload.php');

// Do some checking for the request method here, if desired.
  

然后,代码将尝试从 Bearer 标头中提取令牌。我已经使用preg_match做到了。如果您不熟悉该函数,它会对字符串执行正则表达式匹配

我在这里使用的正则表达式将尝试从 Bearer 标头中提取令牌,并转储其他所有内容。如果未找到,则返回 HTTP 400 错误请求:

  if  (! preg_match('/Bearer\s(\S+)/', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
     header ('HTTP/1.0 400 Bad Request');
    echo 'Token not found in request';
    exit;
}
  

请注意,默认情况下,Apache 不会将 标HTTP_AUTHORIZATION头传递给 PHP。这背后的原因是:

我完全理解这个决定的逻辑。但是,为了避免很多混淆,请将以下内容添加到您的 Apache 配置中。然后代码将按预期运行。如果您使用的是 NGINX ,则代码应按预期运行:

  Rewrite Engine On
 RewriteCond  %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
  

接下来,我们尝试提取匹配的 JWT,它将位于$matches变量的第二个元素中。如果它不可用,则没有提取 JWT,并返回 HTTP 400 错误请求:

 $jwt = $matches[1];
if (! $jwt) {
    // No token was able to be extracted from the authorization header
    header('HTTP/1.0 400 Bad Request');
    exit;
}
  

如果我们到了这一点, 提取了 JWT,因此我们进入解码和验证阶段。为此,我们再次需要我们的密钥,该密钥将从环境或应用程序的配置中提取。然后我们使用 php-jwt 的静态 decode 方法,将 JWT、密钥和用于解码 JWT 的算法数组传递给它。

如果它能够被成功解码,我们就会尝试验证它。我在这里的例子非常简单,因为它只使用发行者,而不是之前和到期时间戳。在实际应用程序中,您可能还会使用许多其他声明。

 $secretKey  = 'bGS6lzFqvvSQ8ALbOxatm7/Vk7mLQyzqaS34Q4oR1ew=';
$token = JWT::decode($jwt, $secretKey, ['HS512']);
$now = new DateTimeImmutable();
$serverName = "your.domain.name";

if ($token->iss !== $serverName ||
    $token->nbf > $now->getTimestamp() ||
    $token->exp < $now->getTimestamp())
{
    header('HTTP/1.1 401 Unauthorized');
    exit;
}
  

如果令牌无效,例如令牌已过期,则会向用户发送 HTTP 401 Unauthorized 标头,并且脚本将退出。

如果解码 JWT 的过程失败,可能是:

  • 提供的段数与前面描述的标准三不匹配。
  • 标头或有效负载不是有效的 JSON 字符串
  • 签名无效,说明数据被篡改!
  • nbf 当当前时间戳小于该时间戳时,该声明在 JWT 中设置为时间戳。
  • iat 当当前时间戳小于该时间戳时,该声明在 JWT 中设置为时间戳。
  • exp 当前时间戳大于该时间戳时,声明会在 JWT 中设置一个时间戳。

如您所见,JWT 有一组很好的控件,可以将其标记为无效,而无需手动撤销它或根据有效令牌列表检查它。

如果解码和验证过程成功,用户将被允许发出请求,并将被发送适当的响应。

综上所述

这是对 JSON Web 令牌或 JWT 以及如何在基于 PHP 的应用程序中使用它们的快速介绍。从这里开始,您可以尝试在您的下一个 API 中实现 JWT,也许可以尝试使用其他一些使用非对称密钥(如 RS256)的签名算法,或者将其集成到现有的 OAUTH2 身份验证服务器中作为 API 密钥。

文章来源:智云一二三科技

文章标题:使用 JWT(JSON Web Token 令牌)进行 PHP 授权

文章地址:https://www.zhihuclub.com/78537.shtml

关于作者: 智云科技

热门文章

网站地图