使用 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
:令牌应该停止有效的时间戳。 应该大于 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 时,它将存储它并在以后对应用程序的每个请求中使用它。
对于一个简单的场景,用户只能请求一个资源——一个恰当命名的 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 的自动加载器。 然后,我们可以选择检查正确的请求方法是否……