ThinkPHP项目令牌刷新与过期

wen PHP项目 3

本文目录导读:

ThinkPHP项目令牌刷新与过期

  1. 使用JWT(推荐方案)
  2. 使用ThinkPHP自带的Session实现
  3. 令牌生命周期管理建议

在ThinkPHP项目中,实现令牌(Token)的刷新与过期管理是一个常见需求,特别是在前后端分离的应用中,以下是几种常见的实现方案:

使用JWT(推荐方案)

安装依赖

composer require firebase/php-jwt

JWT工具类封装

<?php
// app/common/service/JwtService.php
namespace app\common\service;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use think\facade\Cache;
class JwtService
{
    // 密钥
    private $secretKey;
    // 访问令牌有效时间(秒)- 默认2小时
    private $accessTokenExpire = 7200;
    // 刷新令牌有效时间(秒)- 默认7天
    private $refreshTokenExpire = 604800;
    public function __construct()
    {
        $this->secretKey = config('jwt.secret_key');
    }
    /**
     * 生成令牌对
     * @param array $userInfo 用户信息
     * @return array
     */
    public function generateTokenPair($userInfo)
    {
        $accessToken = $this->generateAccessToken($userInfo);
        $refreshToken = $this->generateRefreshToken($userInfo);
        return [
            'access_token' => $accessToken,
            'refresh_token' => $refreshToken,
            'token_type' => 'Bearer',
            'expires_in' => $this->accessTokenExpire
        ];
    }
    /**
     * 生成访问令牌
     */
    public function generateAccessToken($userInfo)
    {
        $payload = [
            'user_id' => $userInfo['id'],
            'user_name' => $userInfo['username'],
            'token_type' => 'access',
            'iat' => time(), // 签发时间
            'exp' => time() + $this->accessTokenExpire // 过期时间
        ];
        return JWT::encode($payload, $this->secretKey, 'HS256');
    }
    /**
     * 生成刷新令牌
     */
    public function generateRefreshToken($userInfo)
    {
        // 生成唯一标识
        $tokenId = md5(uniqid($userInfo['id'], true));
        $payload = [
            'user_id' => $userInfo['id'],
            'token_id' => $tokenId,
            'token_type' => 'refresh',
            'iat' => time(),
            'exp' => time() + $this->refreshTokenExpire
        ];
        // 将刷新令牌ID存储到Redis,便于管理
        $cacheKey = 'refresh_token:' . $tokenId;
        Cache::set($cacheKey, $userInfo['id'], $this->refreshTokenExpire);
        $refreshToken = JWT::encode($payload, $this->secretKey, 'HS256');
        return $refreshToken;
    }
    /**
     * 验证令牌
     * @param string $token 令牌
     * @param string $type 令牌类型
     * @return array|false
     */
    public function verifyToken($token, $type = 'access')
    {
        try {
            $decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
            // 检查令牌类型
            if ($decoded->token_type !== $type) {
                return false;
            }
            // 检查是否过期
            if ($decoded->exp < time()) {
                return false;
            }
            // 如果是刷新令牌,检查是否存在
            if ($type === 'refresh') {
                $cacheKey = 'refresh_token:' . $decoded->token_id;
                if (!Cache::get($cacheKey)) {
                    return false;
                }
            }
            return (array)$decoded;
        } catch (\Exception $e) {
            return false;
        }
    }
    /**
     * 刷新令牌
     * @param string $refreshToken 刷新令牌
     * @return array|false
     */
    public function refreshToken($refreshToken)
    {
        // 验证刷新令牌
        $claims = $this->verifyToken($refreshToken, 'refresh');
        if (!$claims) {
            return false;
        }
        // 获取用户信息
        $userId = $claims['user_id'];
        $user = \think\facade\Db::name('users')->find($userId);
        if (!$user) {
            return false;
        }
        // 删除旧的刷新令牌
        $oldTokenId = $claims['token_id'];
        Cache::delete('refresh_token:' . $oldTokenId);
        // 生成新的令牌对
        return $this->generateTokenPair($user);
    }
    /**
     * 注销令牌
     */
    public function logout($token)
    {
        try {
            $claims = $this->verifyToken($token);
            if ($claims && $claims['token_type'] === 'access') {
                // 简单方案:将access token加入黑名单缓存
                $cacheKey = 'blacklist_token:' . md5($token);
                Cache::set($cacheKey, true, 3600);
            }
            return true;
        } catch (\Exception $e) {
            return false;
        }
    }
}

配置类

<?php
// config/jwt.php
return [
    'secret_key' => env('JWT_SECRET_KEY', 'your-secret-key'),
    'access_token_expire' => env('JWT_ACCESS_EXPIRE', 7200),
    'refresh_token_expire' => env('JWT_REFRESH_EXPIRE', 604800)
];

中间件实现

<?php
// app/middleware/AuthMiddleware.php
namespace app\middleware;
use think\facade\Request;
use think\facade\Config;
use app\common\service\JwtService;
class AuthMiddleware
{
    public function handle($request, \Closure $next)
    {
        // 获取Authorization Header
        $authorization = $request->header('Authorization');
        if (empty($authorization)) {
            return json(['code' => 401, 'message' => '请先登录'], 401);
        }
        // 解析token
        $token = str_replace('Bearer ', '', $authorization);
        $jwtService = new JwtService();
        $claims = $jwtService->verifyToken($token, 'access');
        if (!$claims) {
            return json(['code' => 401, 'message' => '令牌无效或已过期'], 401);
        }
        // 将用户信息绑定到request
        $request->user = $claims;
        return $next($request);
    }
}

控制器使用示例

<?php
// app/controller/Login.php
namespace app\controller;
use app\common\service\JwtService;
use think\facade\Request;
use think\facade\Db;
class Login
{
    protected $jwtService;
    public function __construct()
    {
        $this->jwtService = new JwtService();
    }
    /**
     * 登录接口
     */
    public function login()
    {
        $username = Request::param('username');
        $password = Request::param('password');
        // 验证用户
        $user = Db::name('users')->where('username', $username)->find();
        if (!$user || !password_verify($password, $user['password'])) {
            return json(['code' => 400, 'message' => '用户名或密码错误'], 400);
        }
        // 生成令牌
        $tokenData = $this->jwtService->generateTokenPair($user);
        return json([
            'code' => 200,
            'message' => '登录成功',
            'data' => $tokenData
        ]);
    }
    /**
     * 刷新令牌接口
     */
    public function refresh()
    {
        $refreshToken = Request::param('refresh_token');
        if (empty($refreshToken)) {
            return json(['code' => 400, 'message' => '缺少刷新令牌'], 400);
        }
        $newTokens = $this->jwtService->refreshToken($refreshToken);
        if (!$newTokens) {
            return json(['code' => 401, 'message' => '刷新令牌无效或已过期'], 401);
        }
        return json([
            'code' => 200,
            'message' => '令牌刷新成功',
            'data' => $newTokens
        ]);
    }
    /**
     * 获取用户信息(需要认证)
     */
    public function userInfo()
    {
        $userInfo = Request::user;
        return json([
            'code' => 200,
            'data' => $userInfo
        ]);
    }
    /**
     * 更新用户信息(需要认证)
     */
    public function updateInfo()
    {
        $userInfo = Request::user;
        $userId = $userInfo['user_id'];
        // 更新用户信息逻辑...
        return json([
            'code' => 200,
            'message' => '更新成功'
        ]);
    }
    /**
     * 注销登录
     */
    public function logout()
    {
        $authorization = Request::header('Authorization');
        $token = str_replace('Bearer ', '', $authorization);
        $this->jwtService->logout($token);
        return json([
            'code' => 200,
            'message' => '注销成功'
        ]);
    }
}

路由配置

// route/app.php
use think\facade\Route;
// 公开接口
Route::post('login', 'Login/login');
Route::post('refresh', 'Login/refresh');
// 需要认证的接口
Route::group(function () {
    Route::get('user/info', 'Login/userInfo');
    Route::post('user/update', 'Login/updateInfo');
    Route::post('logout', 'Login/logout');
})->middleware(\app\middleware\AuthMiddleware::class);

使用ThinkPHP自带的Session实现

对于传统Web应用,可以使用基于Session的认证:

<?php
// app/controller/Login.php
namespace app\controller;
use think\facade\Session;
use think\facade\Request;
class Login
{
    public function login()
    {
        // 验证用户...
        // 设置Session
        Session::set('user', [
            'id' => $user['id'],
            'username' => $user['username']
        ]);
        // 设置过期时间(可选)
        Session::set('session_expire', time() + 3600);
        return json(['code' => 200, 'message' => '登录成功']);
    }
    public function checkSession()
    {
        $user = Session::get('user');
        // 检查session是否过期
        $expireTime = Session::get('session_expire');
        if ($expireTime && $expireTime < time()) {
            Session::clear();
            return json(['code' => 401, 'message' => '会话已过期'], 401);
        }
        if (!$user) {
            return json(['code' => 401, 'message' => '未登录'], 401);
        }
        return json(['code' => 200, 'data' => $user]);
    }
    public function logout()
    {
        Session::delete('user');
        Session::delete('session_expire');
        return json(['code' => 200, 'message' => '注销成功']);
    }
}

令牌生命周期管理建议

前端实现逻辑(JavaScript示例)

// token-manager.js
class TokenManager {
    constructor() {
        this.accessToken = null;
        this.refreshToken = null;
        this.accessTokenExpire = 0;
    }
    // 保存令牌
    setTokens(tokens) {
        this.accessToken = tokens.access_token;
        this.refreshToken = tokens.refresh_token;
        this.accessTokenExpire = Date.now() + tokens.expires_in * 1000;
        // 存储到localStorage
        localStorage.setItem('access_token', tokens.access_token);
        localStorage.setItem('refresh_token', tokens.refresh_token);
        localStorage.setItem('token_expire', this.accessTokenExpire);
    }
    // 获取访问令牌
    async getAccessToken() {
        // 检查是否过期
        if (Date.now() >= this.accessTokenExpire) {
            // 尝试刷新
            const refreshed = await this.refresh();
            if (refreshed) {
                return this.accessToken;
            }
            return null;
        }
        return this.accessToken;
    }
    // 刷新令牌
    async refresh() {
        try {
            const response = await fetch('/refresh', {
                method: 'POST',
                body: JSON.stringify({
                    refresh_token: this.refreshToken
                })
            });
            const data = await response.json();
            if (data.code === 200) {
                this.setTokens(data.data);
                return true;
            }
            // 刷新失败,跳转到登录页
            this.logout();
            return false;
        } catch (error) {
            console.error('Token refresh failed:', error);
            return false;
        }
    }
    // 请求拦截
    async request(url, options = {}) {
        const token = await this.getAccessToken();
        if (token) {
            options.headers = {
                ...options.headers,
                'Authorization': `Bearer ${token}`
            };
        }
        return fetch(url, options);
    }
    // 注销
    logout() {
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        localStorage.removeItem('token_expire');
        window.location.href = '/login';
    }
}
// 使用示例
const tokenManager = new TokenManager();
// 登录成功后
tokenManager.setTokens(loginResponse.data);
// 发起请求
tokenManager.request('/user/info')
    .then(res => res.json())
    .then(data => console.log(data));

Token过期策略建议

  1. 双Token方案

    • 访问令牌(Access Token):短期有效(如2小时)
    • 刷新令牌(Refresh Token):长期有效(如7天)
  2. 主动续期策略

    • 在访问令牌即将过期时(如剩余10分钟)自动刷新
    • 在API响应中携带令牌剩余时间
  3. Token撤销机制

    • 用户修改密码后撤销所有Token
    • 用户主动注销时加入黑名单
  4. 安全性增强

    • 使用HTTPS传输
    • 存储时加密Token
    • 限制刷新令牌的使用次数

这样既保证了系统的安全性,又提供了良好的用户体验,根据你的具体业务需求,可以选择相应的实现方案。

抱歉,评论功能暂时关闭!