如何在纯PHP中创建刷新令牌?不依赖OAuth等第三方服务
嘿,我懂你要找的是纯PHP实现刷新令牌的方案,不用依赖OAuth那些第三方服务对吧?其实刷新令牌的核心逻辑比JWT简单不少,咱们一步步来理清楚,结合你现有的JWT代码来补全。
纯PHP实现刷新令牌(无OAuth)
刷新令牌的核心逻辑
首先得明确:刷新令牌是一个长期有效的、加密安全的随机字符串,它的唯一作用就是在JWT过期后,换取新的JWT。和JWT不同,它不携带任何用户数据,只是一个与用户绑定的凭证,而且必须存在服务器端的数据库中(方便验证和作废)。
生成刷新令牌的安全方法
绝对不能用rand()、uniqid()这类不安全的函数生成刷新令牌,必须用PHP提供的加密安全随机函数:
function generateRefreshToken() { // 生成32字节的加密安全随机字符串,转成十六进制后是64位,足够抵御暴力破解 return bin2hex(random_bytes(32)); }
random_bytes()是PHP 7+内置的函数,生成的随机数符合密码学安全标准,完全不用担心被猜测。
存储与配合JWT的完整流程
1. 用户登录时的流程
- 生成短有效期的JWT(比如15-30分钟,避免JWT泄露后被长期滥用)
- 生成长有效期的刷新令牌(比如7-30天,根据业务需求调整)
- 将刷新令牌与用户ID、过期时间、状态(是否作废)一起存入数据库
- 把JWT和刷新令牌返回给客户端(JWT存在localStorage或内存,刷新令牌建议存在HttpOnly+Secure的Cookie中,防止XSS攻击)
2. JWT过期后的刷新流程
- 客户端携带刷新令牌请求专门的
/refresh-token接口 - 服务器验证刷新令牌:检查数据库中是否存在该令牌、是否未过期、是否未被作废
- 验证通过后,生成新的JWT返回给客户端;可选生成新的刷新令牌(刷新令牌轮转,进一步提升安全性)
- 验证失败则返回401,要求用户重新登录
结合你现有JWT代码的完整示例
先补全你未写完的JWT创建函数,再整合刷新令牌的逻辑:
// 建议将密钥存在环境变量中,不要硬编码! $secret = getenv('JWT_SECRET') ?: 'your_strong_random_secret_here'; // JWT头部 $header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']); /** * 创建短有效期的JWT * @param int $user_id 关联的用户ID * @return string 生成的JWT */ function createJWT($user_id) { global $header, $secret; $currentTime = new DateTime(); $iat = $currentTime->getTimestamp(); // 设置JWT有效期为15分钟 $exp = $iat + 15 * 60; // 构建Payload $payload = json_encode([ 'user_id' => $user_id, 'iat' => $iat, // 签发时间 'exp' => $exp // 过期时间 ]); // Base64Url编码(替换JWT不允许的字符) $base64Header = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header)); $base64Payload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload)); // 生成签名 $signature = hash_hmac('sha256', $base64Header . "." . $base64Payload, $secret, true); $base64Signature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature)); // 拼接成最终JWT return $base64Header . "." . $base64Payload . "." . $base64Signature; } /** * 生成加密安全的刷新令牌 * @return string 刷新令牌 */ function generateRefreshToken() { return bin2hex(random_bytes(32)); } /** * 模拟登录接口逻辑 * @param int $user_id 用户ID * @return string JSON响应 */ function handleLogin($user_id) { // 生成JWT $jwt = createJWT($user_id); // 生成刷新令牌 $refreshToken = generateRefreshToken(); // 计算刷新令牌过期时间(7天后) $expiresAt = (new DateTime())->modify('+7 days')->format('Y-m-d H:i:s'); // 将刷新令牌存入数据库(示例用PDO,实际请根据你的数据库连接方式调整) /* $pdo = new PDO('mysql:host=localhost;dbname=your_database', 'username', 'password'); $stmt = $pdo->prepare("INSERT INTO refresh_tokens (user_id, token, expires_at, is_revoked) VALUES (?, ?, ?, 0)"); $stmt->execute([$user_id, $refreshToken, $expiresAt]); */ // 返回给客户端的响应 return json_encode([ 'access_token' => $jwt, 'refresh_token' => $refreshToken, 'token_type' => 'Bearer', 'expires_in' => 15 * 60 // 单位:秒 ]); } /** * 模拟刷新令牌接口逻辑 * @param string $refreshToken 客户端传来的刷新令牌 * @return string JSON响应 */ function handleRefreshToken($refreshToken) { // 从数据库查询刷新令牌的有效性 /* $pdo = new PDO('mysql:host=localhost;dbname=your_database', 'username', 'password'); $stmt = $pdo->prepare("SELECT * FROM refresh_tokens WHERE token = ? AND is_revoked = 0 AND expires_at > NOW()"); $stmt->execute([$refreshToken]); $tokenRecord = $stmt->fetch(PDO::FETCH_ASSOC); */ // 模拟数据库查询结果(实际需从数据库获取) $tokenRecord = [ 'user_id' => $user_id, // 替换为实际用户ID 'token' => $refreshToken ]; if (!$tokenRecord) { // 刷新令牌无效,返回401未授权 http_response_code(401); return json_encode(['error' => 'Invalid or expired refresh token']); } // 生成新的JWT $newJwt = createJWT($tokenRecord['user_id']); // 可选:刷新令牌轮转(生成新的刷新令牌,作废旧的,提升安全性) $newRefreshToken = generateRefreshToken(); $newExpiresAt = (new DateTime())->modify('+7 days')->format('Y-m-d H:i:s'); /* $stmt = $pdo->prepare("UPDATE refresh_tokens SET token = ?, expires_at = ? WHERE token = ?"); $stmt->execute([$newRefreshToken, $newExpiresAt, $refreshToken]); */ // 返回新的令牌 return json_encode([ 'access_token' => $newJwt, 'refresh_token' => $newRefreshToken, // 如果不轮转,返回原$refreshToken即可 'token_type' => 'Bearer', 'expires_in' => 15 * 60 ]); }
关键安全注意事项
- 刷新令牌必须存储在服务器端:不能像JWT那样只靠签名验证,因为刷新令牌是长期有效的,一旦泄露必须能通过数据库标记作废。
- 客户端存储刷新令牌的安全方式:优先使用
HttpOnly、Secure、SameSite属性的Cookie,避免存在localStorage(防止XSS攻击窃取)。 - 密钥安全:绝对不要把JWT密钥硬编码在代码里,用环境变量或者配置文件存储,且密钥要足够长(至少32位随机字符串)。
- 刷新令牌轮转:每次刷新JWT时生成新的刷新令牌,旧的立即作废,这样即使旧令牌泄露也无法被滥用。
- JWT验证:别忘了实现JWT的验证逻辑,每次请求都要检查JWT的签名、过期时间、用户ID是否合法。
内容的提问来源于stack exchange,提问作者Mikael Svensson
相关产品推荐
相关产品推荐

