本文目录导读:

在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过期策略建议
-
双Token方案:
- 访问令牌(Access Token):短期有效(如2小时)
- 刷新令牌(Refresh Token):长期有效(如7天)
-
主动续期策略:
- 在访问令牌即将过期时(如剩余10分钟)自动刷新
- 在API响应中携带令牌剩余时间
-
Token撤销机制:
- 用户修改密码后撤销所有Token
- 用户主动注销时加入黑名单
-
安全性增强:
- 使用HTTPS传输
- 存储时加密Token
- 限制刷新令牌的使用次数
这样既保证了系统的安全性,又提供了良好的用户体验,根据你的具体业务需求,可以选择相应的实现方案。