sizeg/yii2-jwt 扩展本身未直接提供 refresh_token 的生成与管理逻辑(核心仅负责 JWT 令牌的签名/验证),但可基于 JWT 标准和业务需求,手动实现「access_token + refresh_token 双令牌机制」——核心思路是:生成两个功能不同的 JWT 令牌,access_token 短期有效(用于接口访问),refresh_token 长期有效(用于刷新 access_token)。

以下是完整实现方案(兼容 Yii2 2.0.53 版本),包含「生成、存储、刷新、验证」全流程:

一、核心前提

  1. 已安装 sizeg/yii2-jwt 扩展:

    composer require sizeg/yii2-jwt
  2. 已在 Yii2 配置文件中配置 JWT 组件(config/main.php):

    'components' => [
        'jwt' => [
            'class' => \sizeg\jwt\Jwt::class,
            'key' => 'your-secret-key-32bytes-long-123456', // 密钥(建议 ≥32 位,存环境变量)
            'jwtValidationData' => \app\components\JwtValidationData::class, // 自定义验证类
            'algorithm' => \Lcobucci\JWT\Signer\Hmac\Sha256::class, // 加密算法(默认 SHA256)
        ],
    ],
  3. 自定义 JWT 验证类(app/components/JwtValidationData.php):

    namespace app\components;
    
    use sizeg\jwt\JwtValidationData;
    
    class JwtValidationData extends JwtValidationData
    {
        public function init()
        {
            parent::init();
            // 验证发行人(可选,需与生成时一致)
            $this->validateIssuer('https://your-domain.com');
            // 验证受众(可选)
            $this->validateAudience('your-app-name');
        }
    }

二、实现 refresh_token 的核心逻辑

1. 令牌设计原则

令牌类型有效期用途存储方式包含 payload 信息
access_token短期(15-60 分钟)接口访问验证前端存储(localStorage/Header)用户 ID、角色、过期时间等
refresh_token长期(7-30 天)刷新 access_token数据库+前端存储用户 ID、刷新令牌 ID、过期时间

2. 数据库表设计(存储 refresh_token,确保唯一性和可吊销)

创建 user_refresh_token 表(用于存储用户的有效 refresh_token,支持吊销/过期管理):

CREATE TABLE `user_refresh_token` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `refresh_token` varchar(255) NOT NULL COMMENT '刷新令牌(JWT字符串)',
  `token_id` varchar(64) NOT NULL COMMENT '令牌唯一标识(避免重复)',
  `expired_at` int(11) NOT NULL COMMENT '过期时间戳(秒)',
  `created_at` int(11) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` int(11) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1-有效,0-无效(吊销)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_token_id` (`token_id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_expired_at` (`expired_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户刷新令牌表';

3. 生成双令牌(access_token + refresh_token

在用户登录成功后,同时生成两个 JWT 令牌,refresh_token 存入数据库和返回给前端:

namespace app\services;

use app\models\UserRefreshToken;
use sizeg\jwt\Jwt;
use Yii;
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Encoding\Charset;
use Lcobucci\JWT\Encoding\JoseEncoder;

class AuthService
{
    /**
     * 生成双令牌(access_token + refresh_token)
     * @param int $userId 用户ID
     * @return array ['access_token' => string, 'refresh_token' => string, 'expires_in' => int]
     */
    public function generateTokens(int $userId)
    {
        $jwt = Yii::$app->jwt;
        $now = time();

        // -------------------------- 1. 生成 access_token(短期有效,15分钟)--------------------------
        $accessTokenExpire = $now + 900; // 15*60=900秒
        $accessToken = $jwt->getBuilder()
            ->issuedBy('https://your-domain.com') // 发行人(与验证类一致)
            ->permittedFor('your-app-name') // 受众(与验证类一致)
            ->identifiedBy(uniqid('access_', true)) // 令牌唯一ID
            ->issuedAt($now) // 签发时间
            ->expiresAt($accessTokenExpire) // 过期时间
            ->withClaim('user_id', $userId) // 自定义载荷:用户ID
            ->withClaim('token_type', 'access') // 标记令牌类型
            ->getToken($jwt->getSigner(), $jwt->getSigningKey()); // 签名生成令牌

        // -------------------------- 2. 生成 refresh_token(长期有效,7天)--------------------------
        $refreshTokenExpire = $now + 604800; // 7*24*3600=604800秒
        $tokenId = uniqid('refresh_', true); // 刷新令牌唯一标识(用于数据库存储和吊销)
        
        $refreshToken = $jwt->getBuilder()
            ->issuedBy('https://your-domain.com')
            ->permittedFor('your-app-name')
            ->identifiedBy($tokenId) // 关联数据库的 token_id
            ->issuedAt($now)
            ->expiresAt($refreshTokenExpire)
            ->withClaim('user_id', $userId)
            ->withClaim('token_type', 'refresh') // 标记令牌类型(关键:区分 access/refresh)
            ->getToken($jwt->getSigner(), $jwt->getSigningKey());

        // -------------------------- 3. 存储 refresh_token 到数据库(用于后续验证和吊销)--------------------------
        $this->saveRefreshToken($userId, (string)$refreshToken, $tokenId, $refreshTokenExpire);

        // 返回结果(前端存储 access_token 和 refresh_token)
        return [
            'access_token' => (string)$accessToken,
            'refresh_token' => (string)$refreshToken,
            'expires_in' => $accessTokenExpire - $now, // access_token 剩余有效期(秒)
            'refresh_expires_in' => $refreshTokenExpire - $now, // refresh_token 剩余有效期(秒)
        ];
    }

    /**
     * 保存 refresh_token 到数据库
     */
    private function saveRefreshToken(int $userId, string $refreshToken, string $tokenId, int $expiredAt)
    {
        // 1. 先失效该用户之前的所有 refresh_token(可选:单设备登录,避免多令牌)
        UserRefreshToken::updateAll(
            ['status' => 0],
            ['user_id' => $userId, 'status' => 1]
        );

        // 2. 新增当前 refresh_token
        $model = new UserRefreshToken();
        $model->user_id = $userId;
        $model->refresh_token = $refreshToken;
        $model->token_id = $tokenId;
        $model->expired_at = $expiredAt;
        $model->created_at = time();
        $model->save(false); // 无需验证,直接保存
    }
}

4. 登录接口调用(生成令牌)

在控制器中调用服务类,返回双令牌给前端:

namespace app\controllers;

use app\services\AuthService;
use Yii;
use yii\web\Controller;

class AuthController extends Controller
{
    public function actionLogin()
    {
        // 1. 模拟用户登录验证(实际场景:验证用户名密码)
        $userId = 1001; // 登录成功后获取的用户ID

        // 2. 生成双令牌
        $authService = new AuthService();
        $tokens = $authService->generateTokens($userId);

        // 3. 返回结果(前端存储 tokens)
        return $this->asJson([
            'code' => 200,
            'msg' => '登录成功',
            'data' => $tokens,
        ]);
    }
}

三、刷新 access_token(核心:用 refresh_token 换新的 access_token

access_token 过期后,前端用 refresh_token 调用刷新接口,获取新的 access_token(无需重新登录):

namespace app\services;

use app\models\UserRefreshToken;
use sizeg\jwt\Jwt;
use Yii;
use Lcobucci\JWT\Exception\TokenExpiredException;
use Lcobucci\JWT\Exception\TokenInvalidException;

class AuthService
{
    // ... 之前的 generateTokens() 和 saveRefreshToken() 方法 ...

    /**
     * 用 refresh_token 刷新 access_token
     * @param string $refreshToken 前端传入的 refresh_token
     * @return array|null 新的 tokens,失败返回 null
     */
    public function refreshAccessToken(string $refreshToken)
    {
        $jwt = Yii::$app->jwt;

        try {
            // -------------------------- 1. 验证 refresh_token 有效性(JWT 签名+过期)--------------------------
            $token = $jwt->getParser(new JoseEncoder())->parse($refreshToken);
            $validationData = Yii::createObject(\app\components\JwtValidationData::class);
            
            // 验证 JWT 签名、发行人、受众
            if (!$jwt->validateToken($token, $validationData)) {
                Yii::error('refresh_token 验证失败(签名/发行人/受众无效)');
                return null;
            }

            // -------------------------- 2. 提取 refresh_token 载荷信息 --------------------------
            $payload = $token->getClaims();
            $userId = $payload['user_id']->getValue(); // 用户ID
            $tokenType = $payload['token_type']->getValue(); // 令牌类型
            $tokenId = $payload['jti']->getValue(); // 令牌唯一ID(关联数据库)
            $expiredAt = $payload['exp']->getValue(); // 过期时间戳

            // 验证令牌类型是否为 refresh
            if ($tokenType !== 'refresh') {
                Yii::error('无效的令牌类型(非 refresh_token)');
                return null;
            }

            // -------------------------- 3. 验证数据库中的 refresh_token(是否有效/未吊销)--------------------------
            $dbToken = UserRefreshToken::findOne([
                'user_id' => $userId,
                'token_id' => $tokenId,
                'refresh_token' => $refreshToken,
                'status' => 1,
                'expired_at' => ['>', time()], // 未过期
            ]);

            if (!$dbToken) {
                Yii::error('refresh_token 不存在或已失效');
                return null;
            }

            // -------------------------- 4. 生成新的 access_token(可选:同时生成新的 refresh_token,滚动更新)--------------------------
            $newTokens = $this->generateTokens($userId);

            // (可选)失效旧的 refresh_token(滚动更新,增强安全性)
            $dbToken->status = 0;
            $dbToken->save(false);

            return $newTokens;

        } catch (TokenExpiredException $e) {
            Yii::error('refresh_token 已过期:' . $e->getMessage());
            return null;
        } catch (TokenInvalidException $e) {
            Yii::error('refresh_token 无效:' . $e->getMessage());
            return null;
        } catch (\Exception $e) {
            Yii::error('刷新 access_token 失败:' . $e->getMessage());
            return null;
        }
    }
}

刷新接口控制器调用

namespace app\controllers;

use app\services\AuthService;
use Yii;
use yii\web\Controller;

class AuthController extends Controller
{
    public function actionRefreshToken()
    {
        // 1. 获取前端传入的 refresh_token(从 Header 或请求体)
        $refreshToken = Yii::$app->request->post('refresh_token');
        if (!$refreshToken) {
            return $this->asJson([
                'code' => 400,
                'msg' => 'refresh_token 不能为空',
            ]);
        }

        // 2. 刷新 access_token
        $authService = new AuthService();
        $newTokens = $authService->refreshAccessToken($refreshToken);

        if (!$newTokens) {
            return $this->asJson([
                'code' => 401,
                'msg' => 'refresh_token 无效或已过期,请重新登录',
            ]);
        }

        // 3. 返回新的 tokens
        return $this->asJson([
            'code' => 200,
            'msg' => '刷新成功',
            'data' => $newTokens,
        ]);
    }
}

四、关键注意事项(安全+兼容性)

1. 安全性保障

  • refresh_token 必须存储在数据库:用于验证令牌有效性和吊销(如用户退出登录时,标记 status=0);
  • 滚动更新 refresh_token:每次刷新 access_token 时,生成新的 refresh_token 并失效旧的,避免长期有效令牌泄露风险;
  • 密钥安全:JWT 密钥(key)需 ≥32 位,存环境变量(如 getenv('JWT_SECRET_KEY')),禁止硬编码到代码;
  • access_token 短期有效:减少令牌泄露后的风险窗口(建议 15-60 分钟)。

2. 兼容性处理(Yii2 2.0.53 + sizeg/yii2-jwt 版本)

  • sizeg/yii2-jwt 版本较低(如 ≤1.0),getParser() 方法可能不支持 JoseEncoder,需调整解析逻辑:

    // 低版本解析 token 写法(无 JoseEncoder)
    $token = $jwt->getParser()->parse($refreshToken);
  • 确保 Lcobucci\JWT 依赖版本兼容(sizeg/yii2-jwt 通常依赖 lcobucci/jwt:^3.0),若报错需调整 composer.json

    "require": {
        "lcobucci/jwt": "3.4.*" // 固定兼容版本
    }

3. 吊销 refresh_token(用户退出登录)

用户退出时,需将对应的 refresh_token 标记为无效,避免被恶意使用:

public function actionLogout()
{
    $refreshToken = Yii::$app->request->post('refresh_token');
    if (!$refreshToken) {
        return $this->asJson(['code' => 400, 'msg' => 'refresh_token 不能为空']);
    }

    // 查找并失效该 refresh_token
    UserRefreshToken::updateAll(
        ['status' => 0],
        ['refresh_token' => $refreshToken, 'status' => 1]
    );

    return $this->asJson(['code' => 200, 'msg' => '退出成功']);
}

五、总结

sizeg/yii2-jwt 生成 refresh_token 的核心是「手动构建双 JWT 令牌 + 数据库存储 refresh_token」:

  1. access_token 短期有效,用于接口访问;
  2. refresh_token 长期有效,存入数据库,用于刷新 access_token
  3. 刷新时验证 refresh_token 的 JWT 签名、数据库状态、过期时间;
  4. 支持令牌滚动更新和吊销,兼顾安全性和用户体验。

按上述方案实现后,即可完成「登录生成双令牌 → 接口用 access_token 访问 → 过期用 refresh_token 刷新」的完整认证流程。

标签: none

添加新评论