使用 JWT(JSON Web 令牌)的 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 对象表示法 (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:令牌应该停止有效的时间戳。 应该大于 iatnbf.

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

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 时,它将存储它并在以后对应用程序的每个请求中使用它。

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

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

如果您不熟悉 Bearer Authorization,它是一种 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 FirebaseJWTJWT;

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

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

<?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: XMLHttpRequest
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0

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

验证 JWT

最后,让我们看看如何在 PHP 中验证令牌。 一如既往,我们将包括 Composer 的自动加载器。 然后,我们可以选择检查正确的请求方法是否……

阅读更多

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注