深度解析PHP使用JWT实现微信小软件“登录令牌”(Token)

这是一个后端身份认证的核心问题。

首先,我来纠正一个关键点:它不是 Cookie

为什么不是 Cookie?

  1. 运行环境不同:Cookie 是浏览器(Browser)技术,主要用于网页。而微信小程序是在微信 App 内运行的,它有自己独立的一套“沙盒”环境,没有 Cookie 机制
  2. 存储方式不同:在小程序前端,我们不用 Cookie,而是用微信提供的 wx.setStorageSyncwx.setStorage API,把这个 Token 存储在小程序的本地缓存中。

那么这个 Token 到底用什么生成?

现在生成这种“登录令牌”(Token)最主流、最标准、最安全的方式是 JWT (JSON Web Token)

JWT 是什么?
它不是一个随机的字符串,它是一个有结构、自包含、防篡改的“加密身份证”。

一个 JWT Token 看起来像这样(很长,但由三部分组成):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsIm9wZW5pZCI6ImFiYzEyM3h5eiIsImV4cCI6MTcxNjIyNjAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

它由三部分组成,用点 . 隔开:

  1. Header (头部)eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    • (解密后:{"alg":"HS256","typ":"JWT"}
    • 告诉所有人:这是一个 JWT,我是用 HS256 算法加密的。
  2. Payload (载荷)eyJ1c2VyX2lkIjoxMjMsIm9wZW5pZCI6ImFiYzEyM3h5eiIsImV4cCI6MTcxNjIyNjAwMH0
    • (解密后:{"user_id":123,"openid":"abc123xyz","exp":1716226000}
    • 这就是核心! 这就是你的“会员卡”信息。后端把用户的关键信息(比如你在数据库里的 user_idopenid)和过期时间exp,这就是“时效性”)都放在这里。
  3. Signature (签名)SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    • 这是防伪标识。后端会用一个只有自己知道的“秘钥”(Secret),把前两部分加密生成这个签名。
    • 好处是:如果前端或黑客篡改了 Payload(比如想把 user_id: 123改成user_id: 456),那么签名就会对不上,后端会立刻识破,认证失败。

为什么用 JWT?

  • 无状态 (Stateless):后端不需要session 一样在服务器上存东西。所有信息都在 Token 里,服务器拿到 Token 解密就知道“你是谁”,非常适合高并发和分布式系统。
  • 防篡改 (Tamper-proof):有签名保护,载荷内容绝对可信。

PHP Demo:如何生成和验证 JWT

在 PHP 中,我们不自己写 JWT,而是使用标准库。最常用的是 firebase/php-jwt

第一步:安装扩展包

在你的 PHP 项目根目录下,使用 Composer 安装:

composer require firebase/php-jwt
第二步:编写生成 Token 的 Demo (登录接口)

假设这是你的 login.php 文件,它在拿到前端发来的 code 并换取到 OpenID 之后执行:

<?php
// 引入 composer 的自动加载
require_once 'vendor/autoload.php';
// 引入 JWT 库
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// --- 模拟登录成功后拿到的用户信息 ---
$openid = 'abc123xyz'; // 这是你从微信服务器换来的
$userIdInDb = 123;     // 这是你在自己数据库里的用户ID
// --- 开始生成 JWT ---
// 1. 定义一个超级机密的秘钥 (必须藏好,不能泄露!)
// 实际项目中,这个值应该写在配置文件中,而不是代码里
$secretKey = 'your-super-secret-and-long-key-string-123456!';
// 2. 准备 Payload (载荷)
$issueTime = time();             // 签发时间
$expireTime = $issueTime + (3600 * 24 * 7); // 过期时间 (这里设为 7 天)
$payload = [
'iss'  => 'https://yourdomain.com', // 签发者 (可选)
'aud'  => 'https://myapp.com',      // 接收者 (可选)
'iat'  => $issueTime,               // 签发时间
'exp'  => $expireTime,              // 过期时间
'data' => [                         // 真正要传递的自定义数据
'user_id' => $userIdInDb,
'openid'  => $openid
]
];
// 3. 生成 Token
$token = JWT::encode($payload, $secretKey, 'HS256');
// 4. 返回给前端
header('Content-Type: application/json');
echo json_encode([
'code' => 0,
'message' => '登录成功',
'data' => [
'token' => $token, // 把这个令牌发给前端
'userInfo' => [
'user_id' => $userIdInDb,
'nickname' => '张三' // 假设从数据库读了昵称
]
]
]);
?>

前端拿到这个 token 字符串后,就会通过 wx.setStorageSync('token', token) 存起来。


第三步:如何验证 Token (业务接口)

现在,用户登录成功了。当他要访问“我的订单”(getOrder.php)时,前端会在请求头 (Header) 里带上这个 token

Authorization: Bearer <...长长的token字符串...>

后端需要一个“中间件”或在每个接口的开头来验证这个 token

<?php
// getOrder.php - 模拟一个需要登录才能访问的接口
require_once 'vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
// 同样的秘钥,必须和生成时一致
$secretKey = 'your-super-secret-and-long-key-string-123456!';
// 1. 从请求头获取 Token (这里做个简单模拟)
$headers = getallheaders();
$token = null;
if (isset($headers['Authorization'])) {
$matches = [];
if (preg_match('/Bearer\s(\S+)/', $headers['Authorization'], $matches)) {
$token = $matches[1];
}
}
if (!$token) {
http_response_code(401);
echo json_encode(['message' => '缺少登录凭证']);
exit;
}
// 2. 尝试解码和验证 Token
try {
// JWT::decode 会自动做三件事:
// 1. 验证签名是否被篡改
// 2. 验证是否已过期 (exp)
// 3. 如果都通过,返回解码后的 $payload
$decoded = JWT::decode($token, new Key($secretKey, 'HS256'));
// 3. 验证成功!$decoded 里就是我们当初存的数据
$currentUserId = $decoded->data->user_id;
// --- 开始处理业务逻辑 ---
// 比如:去数据库查询 $currentUserId 的订单...
$orders = [
['id' => 1, 'product' => '篮球', 'user_id' => $currentUserId],
['id' => 2, 'product' => '足球', 'user_id' => $currentUserId]
];
header('Content-Type: application/json');
echo json_encode(['code' => 0, 'data' => $orders]);
// --- 业务逻辑结束 ---
} catch (ExpiredException $e) {
// 令牌过期了
http_response_code(401); // 401 Unauthorized
echo json_encode(['message' => '登录已过期,请重新登录', 'error_code' => 1001]);
} catch (SignatureInvalidException $e) {
// 签名无效 (令牌被篡改)
http_response_code(401);
echo json_encode(['message' => '登录凭证无效', 'error_code' => 1002]);
} catch (Exception $e) {
// 其他所有错误
http_response_code(401);
echo json_encode(['message' => '登录凭证无效', 'error' => $e->getMessage()]);
}
?>
posted on 2026-02-10 11:27  ljbguanli  阅读(18)  评论(0)    收藏  举报