如何在 WordPress 中生成和验证 JWT(JSON Web Token)
好吧,终于在 WPJAM Basic 中实现了 JWT 登录和认证功能,今天就整理一下,首先按照惯例做一些 JWT 的介绍。
什么是 JWT
JWT(JSON Web Token)是一种轻量级的、自包含的、通常用于身份验证和授权的令牌,它将用户信息(如用户ID、角色和权限等)编码到一个 JSON 对象中,然后对其进行数字签名,从而生成一个经过签名的令牌。
由于 JWT 在客户端和服务器之间传输时是加密的,因此服务器可以轻松验证该令牌的有效性和真实性。JWT 常用于无状态的 RESTful API,作为访问受保护资源的凭据,可以在每次请求时附加在请求头中,下图是使用 JWT 的一个常见的交互流程:
JWT 的结构
JWT(JSON Web Token)由三部分组成,分别是:Header(头部)、Payload(负载)和 Signature(签名)。
这三部分使用点(.)分隔,合并为一个字符,如下所示:
header.payload.signature
Header(头部):
Header 是一个 JSON 对象,通常包含两个属性:alg
和 typ
,alg
属性表示签名算法(如 HS256、RS256 等),typ
属性表示令牌类型,通常为 "JWT"。Header 会被 Base64Url 编码,得到一个字符串。例如:
{
"alg": "HS256",
"typ": "JWT"
}
Payload(负载):
Payload 是一个 JSON 对象,包含一些“声明”(Claim),用于传递用户信息和其他业务数据,声明可以是预定义的(如 iss、exp、sub 等),也可以是自定义的,Payload 会被 Base64Url 编码,得到一个字符串。例如:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
下面这 7 个字段都是由官方所定义的,也就是预定义(Registered claims)的,但并不都是必需的,
- iss (issuer):签发人
- sub (subject):主题
- aud (audience):受众
- exp (expiration time):过期时间
- nbf (Not Before):生效时间,在此之前是无效的
- iat (Issued At):签发时间
- jti (JWT ID):唯一身份标识,主要用来作为一次性 token,从而回避重放攻击
另外声明名称只有三个字符,因为 JWT 意味着是紧凑的。
Signature(签名):
签名用于保证 JWT 的完整性和安全性,它是将 Header、Payload 和一个密钥(Secret)通过签名算法进行加密得到的,签名可以防止 JWT 被篡改和伪造,例如,使用 HMAC-SHA256 算法生成签名的方法如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
将 Header
、Payload
和 Signature
用点(.)连接起来,就得到了一个完整的 JWT,例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
在 WordPress 中实现 JWT
通过上面 JWT 的介绍和 JWT 结构可知,生成 JWT,就是通过签名算法和一个用于生成签名的密钥(Secret
)对 Payload
生成签名:我们为了方便就只支持 SHA256 签名算法:
function wpjam_generate_jwt($payload, $secret='', $header=[]){
//无法生成没有设置过期时间的 JWT
if(empty($payload['exp'])){
return false;
}
$header = wp_parse_args($header, [
'alg' => 'HS256',
'typ' => 'JWT'
]);
if($header['alg'] == 'HS256'){
$header = base64_urlencode(wpjam_json_encode($header));
$payload = base64_urlencode(wpjam_json_encode($payload));
$jwt = $header.'.'.$payload;
$secret = $secret ?: wp_salt();
return $jwt.'.'.base64_urlencode(hash_hmac('sha256', $jwt, $secret, true));
}
}
上面的代码首先对 Header
和 Payload
进行 JSON 编码和 URL 安全的 Base64 编码,生成签名的密钥(Secret
)如果为空,则使用 WordPress 默认的盐值函数来生成,最后把 Header
和 Payload
和生成的签名通过点(.)连接起来得到了一个完整的 JWT 。
那么怎么验证 JWT 呢,验证的过程就是生成的反过程:首先通过点(.)将 JWT 分割成 Header
、Payload
和 Signature
三段,然后对 Header
和 Payload
进行 URL 安全的 Base64 解码和JSON 解码,通过签名的密钥最后验证签名:
function wpjam_verify_jwt($token, $secret=''){
$tokens = explode('.', $token);
if(count($tokens) != 3){
return false;
}
list($header, $payload, $sign) = $tokens;
$jwt = $header.'.'.$payload;
$secret = $secret ?: wp_salt();
$header = wpjam_json_decode(base64_urldecode($header));
$payload = wpjam_json_decode(base64_urldecode($payload));
if(empty($header['alg']) || $header['alg'] != 'HS256'){
return false;
}
if(!hash_equals(base64_urlencode(hash_hmac('sha256', $jwt, $secret, true)), $sign)){
return false;
}
//签发时间大于当前服务器时间验证失败
if(isset($payload['iat']) && $payload['iat'] > time()){
return false;
}
//该nbf时间之前不接收处理该Token
if(isset($payload['nbf']) && $payload['nbf'] > time()){
return false;
}
//没有设置过期时间,或过期时间小于当前服务器时间验证失败
if(empty($payload['exp']) || $payload['exp'] < time()){
return false;
}
return $payload;
}
我们这边生成和验证函数有点特殊处理,就是都要实现过期时间设置,意思就是不能设置没有过期时间的 JWT。
wpjam_generate_jwt
和 wpjam_verify_jwt
这两个函数在 WPJAM Basic 中已经内置了,通过他们就可以在 WordPress 实现 JWT 的生成和验证了。
特别提醒一下过程中用到的 URL 安全的 Base64 编码和解码函数 base64_urlencode
和 base64_urldecode
也是在 WPJAM Basic 中定义了。