Skip to content

JWT 开发指南

概述

DSPlatform 使用 JWT (JSON Web Token) 进行用户身份认证和授权。JWT 是一种无状态的认证机制,通过签名确保 token 的完整性和安全性。

技术架构

核心组件

  • JwtToken: JWT token 生成和验证类
  • TokenCache: Token 版本控制和失效管理类
  • Firebase JWT: 第三方 JWT 库,用于 token 的编码和解码

依赖库

json
{
    "firebase/php-jwt": "^6.0"
}

JWT 实现

JwtToken 类

构造函数

php
// app/deshang/utils/JwtToken.php
class JwtToken
{
    protected $jwt_key;
    
    public function __construct()
    {
        $this->jwt_key = env('JWT_SECRET_KEY');
        if (empty($this->jwt_key)) {
            throw new \Exception('JWT密钥未配置,请检查.env文件中的JWT_SECRET设置');
        }
    }
}

生成 Token

php
/**
 * 生成新的令牌(自动添加版本控制)
 * 
 * @param array $payload_data Token载荷数据
 * @param int $expire 过期时间(秒)
 * @return array
 */
public function generateToken($payload_data, $expire)
{
    // 如果payload包含role和user_id,自动添加版本号
    if (isset($payload_data['role']) && isset($payload_data['user_id'])) {
        $payload_data['token_version'] = (new TokenCache())->getCurrentVersion(
            $payload_data['role'], 
            $payload_data['user_id']
        );
    }

    $payload = [
        'iss' => request()->host(true), // 签发者
        'aud' => request()->host(true), // 接收者
        'iat' => time(), // 签发时间
        'nbf' => time(), // 在此之前不可用
        'exp' => time() + $expire, // 过期时间
        'data' => $payload_data,
    ];
    
    $token = JWT::encode($payload, $this->jwt_key, 'HS256');
    return [
        'token' => $token,
        'exp' => $payload['exp'],
    ];
}

验证 Token

php
/**
 * 验证令牌(自动检查版本)
 * 
 * @param string $token JWT Token
 * @return array|false
 */
public function validateToken($token)
{
    try {
        $decoded = JWT::decode($token, new Key($this->jwt_key, 'HS256'));
        $token_data = (array) $decoded->data;
        
        // 如果token包含版本信息,自动验证版本
        if (isset($token_data['role']) && isset($token_data['user_id']) && isset($token_data['token_version'])) {
            if (!(new TokenCache())->validateTokenVersion(
                $token_data['role'], 
                $token_data['user_id'], 
                $token_data['token_version']
            )) {
                return false; // Token版本不匹配,已失效
            }
        }
        
        return $token_data;
    } catch (\Exception $e) {
        return false;
    }
}

刷新 Token

php
/**
 * 刷新令牌
 * 
 * @param string $token JWT Token
 * @return string|false
 */
public function refreshToken($token)
{
    $jwt_expire = 3600 * 5; // 5小时
    try {
        $decoded = JWT::decode($token, new Key($this->jwt_key, 'HS256'));
        $payload = (array) $decoded;
        $payload['iat'] = time();
        $payload['nbf'] = time();
        $payload['exp'] = time() + $jwt_expire;
        return JWT::encode($payload, $this->jwt_key, 'HS256');
    } catch (\Exception $e) {
        return false;
    }
}

TokenCache 类

版本验证

php
// app/deshang/utils/TokenCache.php
class TokenCache
{
    const TOKEN_VERSION_PREFIX = 'token_version_';
    const DEFAULT_EXPIRE = 3600 * 24 * 7; // 7天

    /**
     * 验证Token版本是否有效
     */
    public function validateTokenVersion(string $role, int $user_id, int $token_version): bool
    {
        $cache_key = self::TOKEN_VERSION_PREFIX . "{$role}_{$user_id}";
        $current_version = CacheUtil::get($cache_key, 1);
        
        return $current_version == $token_version;
    }

    /**
     * 使所有Token失效(递增版本号)
     */
    public function invalidateToken(string $role, int $user_id): void
    {
        $cache_key = self::TOKEN_VERSION_PREFIX . "{$role}_{$user_id}";
        $current_version = CacheUtil::get($cache_key, 1);
        $new_version = $current_version + 1;
        CacheUtil::set($cache_key, $new_version, self::DEFAULT_EXPIRE);
    }

    /**
     * 获取当前Token版本号
     */
    public function getCurrentVersion(string $role, int $user_id): int
    {
        $cache_key = self::TOKEN_VERSION_PREFIX . "{$role}_{$user_id}";
        return CacheUtil::get($cache_key, 1);
    }
}

Token 类型

Access Token

用途: 用于 API 访问认证
有效期: 1小时
载荷内容: 用户详细信息

用户端 Access Token

php
$access_payload_data = [
    'role' => 'user',
    'user_id' => $user_info['id'],
    'username' => $user_info['username'],
    'user_is_enabled' => $user_info['is_enabled'],
    'merchant_id' => $user_info['merchant']['id'] ?? 0,
    'merchant_is_enabled' => $user_info['merchant']['is_enabled'] ?? 0,
    'rider_id' => $user_info['rider']['id'] ?? 0,
    'rider_is_enabled' => $user_info['rider']['is_enabled'] ?? 0,
    'technician_id' => $user_info['technician']['id'] ?? 0,
    'technician_is_enabled' => $user_info['technician']['is_enabled'] ?? 0,
    'blogger_id' => $user_info['blogger']['id'] ?? 0,
    'blogger_is_enabled' => $user_info['blogger']['is_enabled'] ?? 0,
];

管理员端 Access Token

php
$access_payload_data = [
    'role' => 'admin',
    'user_id' => $adminInfo['id'],
    'username' => $adminInfo['username'],
    'admin_is_super' => $adminInfo['is_super'],
];

Refresh Token

用途: 用于刷新 Access Token
有效期: 7天
载荷内容: 基础用户信息

php
$refresh_payload_data = [
    'role' => 'user', // 或 'admin'
    'user_id' => $user_info['id'],
];

认证流程

登录流程

  1. 用户登录验证

    • 验证用户名/密码或手机号/验证码
    • 检查用户状态和权限
  2. 生成 Token

    • 生成 Access Token(1小时有效期)
    • 生成 Refresh Token(7天有效期)
    • 自动添加版本号
  3. 返回 Token 信息

    php
    $data = [
        'access_token' => $access_token['token'],
        'access_expires_time' => $access_token['exp'],
        'refresh_token' => $refresh_token['token'],
        'refresh_expires_time' => $refresh_token['exp'],
        'userinfo' => $user_info
    ];

Token 验证流程

  1. 提取 Token

    • 从请求头 access-token 获取 token
  2. 验证 Token

    • 验证签名和过期时间
    • 检查版本号是否匹配
  3. 注入用户信息

    • 将用户信息注入到请求对象中

Token 刷新流程

  1. 验证 Refresh Token

    • 检查 refresh token 的有效性
  2. 生成新的 Access Token

    • 使用 refresh token 中的用户信息
    • 生成新的 access token
  3. 返回新 Token

    • 返回新的 access token 和过期时间

中间件集成

用户认证中间件

php
// app/api/middleware/UserAuthorizeToken.php
class UserAuthorizeToken
{
    public function handle(Request $request, \Closure $next)
    {
        $token = $request->header('access-token');

        if (strlen($token ?? '') > 0) {
            $token_info = (new JwtToken())->validateToken($token);

            if ($token_info == false) {
                return ds_json_error('登录状态已失效,请重新登录', [], 10001, 401);
            }

            if ($token_info['role'] != 'user') {
                return ds_json_error('身份验证失败,请重新登录', [], 10001, 403);
            }

            // 设置用户信息到请求对象
            $request->user_id = $token_info['user_id'] ?? 0;
            $request->username = $token_info['username'] ?? '';
            $request->user_is_enabled = $token_info['user_is_enabled'] ?? 0;
            $request->merchant_id = $token_info['merchant_id'] ?? 0;
            $request->merchant_is_enabled = $token_info['merchant_is_enabled'] ?? 0;
            $request->rider_id = $token_info['rider_id'] ?? 0;
            $request->rider_is_enabled = $token_info['rider_is_enabled'] ?? 0;
            $request->technician_id = $token_info['technician_id'] ?? 0;
            $request->technician_is_enabled = $token_info['technician_is_enabled'] ?? 0;
            $request->blogger_id = $token_info['blogger_id'] ?? 0;
            $request->blogger_is_enabled = $token_info['blogger_is_enabled'] ?? 0;
        }

        return $next($request);
    }
}

管理员认证中间件

php
// app/adminapi/middleware/AdminAuthorizeToken.php
class AdminAuthorizeToken
{
    public function handle(Request $request, \Closure $next)
    {
        $token = $request->header('access-token');

        if (strlen($token ?? '') > 0) {
            $token_info = (new JwtToken())->validateToken($token);

            if ($token_info == false) {
                return ds_json_error('登录状态已失效,请重新登录', [], 10001, 401);
            }

            if ($token_info['role'] != 'admin') {
                return ds_json_error('身份验证失败,请重新登录', [], 10001, 403);
            }

            // 设置管理员信息到请求对象
            $request->admin_id = $token_info['user_id'] ?? 0;
            $request->username = $token_info['username'] ?? '';
            $request->admin_is_super = $token_info['admin_is_super'] ?? 0;
        }

        return $next($request);
    }
}

配置说明

环境变量

env
# JWT 密钥(必须配置)
JWT_SECRET_KEY=your-secret-key-here

安全建议

  1. 密钥管理

    • 使用强随机密钥
    • 定期更换密钥
    • 不同环境使用不同密钥
  2. Token 安全

    • 使用 HTTPS 传输
    • 设置合理的过期时间
    • 实现版本控制机制
  3. 版本控制

    • 支持强制失效所有 token
    • 防止 token 重放攻击
    • 支持单点登录控制

使用示例

生成 Token

php
use app\deshang\utils\JwtToken;

$jwt = new JwtToken();

// 生成用户 token
$user_payload = [
    'role' => 'user',
    'user_id' => 123,
    'username' => 'testuser',
    'user_is_enabled' => 1,
];

$token_result = $jwt->generateToken($user_payload, 3600);
echo $token_result['token']; // JWT token
echo $token_result['exp'];   // 过期时间戳

验证 Token

php
use app\deshang\utils\JwtToken;

$jwt = new JwtToken();
$token_info = $jwt->validateToken($token);

if ($token_info !== false) {
    echo "用户ID: " . $token_info['user_id'];
    echo "用户名: " . $token_info['username'];
    echo "角色: " . $token_info['role'];
} else {
    echo "Token 无效";
}

失效 Token

php
use app\deshang\utils\TokenCache;

$tokenCache = new TokenCache();
$tokenCache->invalidateToken('user', 123); // 使用户ID为123的所有token失效

错误处理

常见错误

  1. Token 过期

    • 错误码: 401
    • 处理: 使用 refresh token 刷新
  2. Token 无效

    • 错误码: 401
    • 处理: 重新登录
  3. 权限不足

    • 错误码: 403
    • 处理: 检查用户角色和权限
  4. 版本不匹配

    • 错误码: 401
    • 处理: 重新登录

错误响应格式

php
// Token 失效
return ds_json_error('登录状态已失效,请重新登录', [], 10001, 401);

// 权限不足
return ds_json_error('身份验证失败,请重新登录', [], 10001, 403);

最佳实践

1. Token 存储

  • 前端: 存储在 localStorage 或 sessionStorage
  • 后端: 不存储 token,只验证签名

2. 安全传输

  • 使用 HTTPS 协议
  • 在请求头中传递 token
  • 避免在 URL 中传递敏感信息

3. 过期处理

  • Access Token 短期有效(1小时)
  • Refresh Token 长期有效(7天)
  • 实现自动刷新机制

4. 版本控制

  • 支持强制失效
  • 防止 token 重放
  • 支持单点登录

5. 监控和日志

  • 记录 token 生成和验证日志
  • 监控异常访问
  • 统计 token 使用情况

最后更新:2024-01-20
维护者:DSPlatform技术团队