ThinkPHP6 JWT认证中间件实战:无痛刷新Token机制
Orion K Lv6

在现代Web应用开发中,JWT(JSON Web Token)已经成为API认证的主流方案。本文将详细介绍如何在ThinkPHP6中实现JWT认证中间件,并实现无痛刷新Token机制,提升用户体验。

JWT认证的优势

JWT相比传统的Session认证具有以下优势:

  • 无状态性:服务器不需要存储会话信息
  • 跨域支持:适合分布式系统和微服务架构
  • 移动端友好:特别适合移动应用和单页应用
  • 安全性:通过签名机制保证数据完整性

环境准备

安装JWT扩展包

推荐使用 thans/tp-jwt-auth 包,它专为ThinkPHP框架优化:

1
composer require thans/tp-jwt-auth

生成JWT配置

执行以下命令生成配置文件:

1
php think jwt:create

此命令会生成 config/jwt.php 配置文件,并在 .env 文件中添加随机生成的密钥。

JWT配置详解

基础配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// config/jwt.php
return [
// JWT密钥
'secret' => env('JWT_SECRET'),

// 算法类型
'algo' => 'HS256',

// Token有效期(秒)
'ttl' => 7200,

// 刷新Token的宽限期(秒)
'refresh_ttl' => 20160,

// 黑名单宽限期(秒)
'blacklist_grace_period' => 30,

// 自动刷新
'auto_refresh' => true,
];

环境变量配置

.env 文件中添加:

1
2
3
4
# JWT配置
JWT_SECRET=your_secret_key_here
JWT_TTL=7200
JWT_REFRESH_TTL=20160

创建JWT认证中间件

生成中间件

1
php think make:middleware JWTAuth

中间件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php
declare(strict_types=1);

namespace app\middleware;

use thans\jwt\exception\JWTException;
use thans\jwt\exception\TokenBlacklistException;
use thans\jwt\exception\TokenBlacklistGracePeriodException;
use thans\jwt\exception\TokenExpiredException;
use thans\jwt\middleware\JWTAuth as BaseJWTAuth;
use think\exception\HttpException;
use think\Response;

/**
* JWT认证中间件
* 支持自动刷新Token机制
*/
class JWTAuth extends BaseJWTAuth
{
/**
* 处理请求
* @param \think\Request $request 请求对象
* @param \Closure $next 下一个中间件
* @return Response 响应对象
* @throws HttpException 认证失败异常
*/
public function handle($request, \Closure $next): Response
{
$token = null;

try {
// 验证Token
$payload = $this->auth->auth();
} catch (TokenExpiredException $e) {
// Token过期,尝试刷新
try {
$this->auth->setRefresh();
$token = $this->auth->refresh();
$payload = $this->auth->auth(false);
} catch (TokenBlacklistGracePeriodException $e) {
// 在宽限期内,允许使用
$payload = $this->auth->auth(false);
} catch (JWTException $exception) {
throw new HttpException(401, '认证失败:' . $exception->getMessage());
}
} catch (TokenBlacklistGracePeriodException $e) {
// 在黑名单宽限期内
$payload = $this->auth->auth(false);
} catch (TokenBlacklistException $e) {
throw new HttpException(401, '用户未登录或Token已失效');
} catch (JWTException $e) {
throw new HttpException(401, '认证失败:' . $e->getMessage());
}

// 将用户ID注入到请求中
$request->uid = $payload['uid']->getValue();

// 执行下一个中间件
$response = $next($request);

// 如果有新Token,添加到响应头
if (isset($token)) {
$this->setAuthentication($response, $token);
}

return $response;
}

/**
* 设置认证响应头
* @param Response $response 响应对象
* @param string $token 新Token
*/
protected function setAuthentication(Response $response, string $token): void
{
$response->header([
'Authorization' => 'Bearer ' . $token,
'Access-Control-Expose-Headers' => 'Authorization'
]);
}
}

用户登录接口实现

登录控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<?php
declare(strict_types=1);

namespace app\api\controller;

use app\api\model\User;
use app\BaseController;
use thans\jwt\facade\JWTAuth;
use think\Request;
use think\Response;

/**
* 用户认证控制器
*/
class Auth extends BaseController
{
/**
* 用户登录
* @param Request $request 请求对象
* @return Response JSON响应
*/
public function login(Request $request): Response
{
// 获取登录参数
$username = $request->post('username');
$password = $request->post('password');

// 参数验证
if (empty($username) || empty($password)) {
return json([
'code' => 400,
'message' => '用户名和密码不能为空'
]);
}

// 验证用户凭据
$user = User::where('username', $username)->find();

if (!$user || !password_verify($password, $user->password)) {
return json([
'code' => 401,
'message' => '用户名或密码错误'
]);
}

// 生成JWT Token
$token = JWTAuth::builder([
'uid' => $user->id,
'username' => $user->username,
'role' => $user->role
]);

// 更新最后登录时间
$user->last_login_time = time();
$user->save();

return json([
'code' => 200,
'message' => '登录成功',
'data' => [
'token' => 'Bearer ' . $token,
'user' => [
'id' => $user->id,
'username' => $user->username,
'nickname' => $user->nickname
]
]
]);
}

/**
* 用户登出
* @param Request $request 请求对象
* @return Response JSON响应
*/
public function logout(Request $request): Response
{
try {
// 将当前Token加入黑名单
JWTAuth::invalidate();

return json([
'code' => 200,
'message' => '登出成功'
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'message' => '登出失败:' . $e->getMessage()
]);
}
}

/**
* 刷新Token
* @return Response JSON响应
*/
public function refresh(): Response
{
try {
$token = JWTAuth::refresh();

return json([
'code' => 200,
'message' => 'Token刷新成功',
'data' => [
'token' => 'Bearer ' . $token
]
]);
} catch (\Exception $e) {
return json([
'code' => 401,
'message' => 'Token刷新失败:' . $e->getMessage()
]);
}
}
}

路由配置

API路由设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// route/api.php
use think\facade\Route;

// 公开路由(无需认证)
Route::group('auth', function () {
Route::post('login', 'Auth/login');
Route::post('register', 'Auth/register');
});

// 需要认证的路由
Route::group('api', function () {
// 用户相关
Route::get('user/profile', 'User/profile');
Route::put('user/profile', 'User/updateProfile');

// 认证相关
Route::post('auth/logout', 'Auth/logout');
Route::post('auth/refresh', 'Auth/refresh');

})->middleware(\app\middleware\JWTAuth::class);

前端集成方案

Axios拦截器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// axios配置
import axios from 'axios';

// 请求拦截器
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = token;
}
return config;
},
error => {
return Promise.reject(error);
}
);

// 响应拦截器
axios.interceptors.response.use(
response => {
// 检查是否有新的Token
const newToken = response.headers.authorization;
if (newToken) {
localStorage.setItem('token', newToken);
}
return response;
},
error => {
// 检查错误响应中是否有新Token
if (error.response && error.response.headers.authorization) {
localStorage.setItem('token', error.response.headers.authorization);
}

// 处理401错误
if (error.response && error.response.status === 401) {
localStorage.removeItem('token');
// 跳转到登录页
window.location.href = '/login';
}

return Promise.reject(error);
}
);

Vue.js组合式API示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// composables/useAuth.js
import { ref, computed } from 'vue';
import axios from 'axios';

const token = ref(localStorage.getItem('token'));
const user = ref(null);

export function useAuth() {
const isAuthenticated = computed(() => !!token.value);

/**
* 用户登录
* @param {Object} credentials 登录凭据
* @returns {Promise} 登录结果
*/
const login = async (credentials) => {
try {
const response = await axios.post('/auth/login', credentials);

if (response.data.code === 200) {
token.value = response.data.data.token;
user.value = response.data.data.user;
localStorage.setItem('token', token.value);
return { success: true, data: response.data };
}

return { success: false, message: response.data.message };
} catch (error) {
return {
success: false,
message: error.response?.data?.message || '登录失败'
};
}
};

/**
* 用户登出
* @returns {Promise} 登出结果
*/
const logout = async () => {
try {
await axios.post('/api/auth/logout');
} catch (error) {
console.error('登出请求失败:', error);
} finally {
token.value = null;
user.value = null;
localStorage.removeItem('token');
}
};

return {
token,
user,
isAuthenticated,
login,
logout
};
}

高级特性

多设备登录管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
// app/service/TokenService.php
namespace app\service;

use think\facade\Cache;
use thans\jwt\facade\JWTAuth;

/**
* Token管理服务
*/
class TokenService
{
/**
* 限制用户同时在线设备数量
* @param int $userId 用户ID
* @param string $token 当前Token
* @param int $maxDevices 最大设备数
*/
public function limitUserDevices(int $userId, string $token, int $maxDevices = 3): void
{
$cacheKey = "user_tokens:{$userId}";
$userTokens = Cache::get($cacheKey, []);

// 添加当前Token
$userTokens[] = $token;

// 如果超过限制,移除最旧的Token
if (count($userTokens) > $maxDevices) {
$oldTokens = array_slice($userTokens, 0, -$maxDevices);

// 将旧Token加入黑名单
foreach ($oldTokens as $oldToken) {
try {
JWTAuth::setToken($oldToken)->invalidate();
} catch (\Exception $e) {
// 忽略已失效的Token
}
}

$userTokens = array_slice($userTokens, -$maxDevices);
}

// 更新缓存
Cache::set($cacheKey, $userTokens, 7200);
}
}

Token权限验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
// app/middleware/PermissionCheck.php
namespace app\middleware;

use think\Request;
use think\Response;
use think\exception\HttpException;

/**
* 权限检查中间件
*/
class PermissionCheck
{
/**
* 处理请求
* @param Request $request 请求对象
* @param \Closure $next 下一个中间件
* @param string $permission 所需权限
* @return Response 响应对象
* @throws HttpException 权限不足异常
*/
public function handle(Request $request, \Closure $next, string $permission = ''): Response
{
$userId = $request->uid;

if (!$this->checkPermission($userId, $permission)) {
throw new HttpException(403, '权限不足');
}

return $next($request);
}

/**
* 检查用户权限
* @param int $userId 用户ID
* @param string $permission 权限标识
* @return bool 是否有权限
*/
private function checkPermission(int $userId, string $permission): bool
{
// 这里实现具体的权限检查逻辑
// 可以从数据库或缓存中获取用户权限
return true;
}
}

性能优化建议

1. 缓存用户信息

1
2
3
4
// 在JWT中间件中缓存用户信息
$userInfo = Cache::remember("user_info:{$userId}", function() use ($userId) {
return User::find($userId);
}, 300); // 缓存5分钟

2. 异步日志记录

1
2
3
4
5
6
7
8
// 记录认证日志
Queue::push('app\job\AuthLog', [
'user_id' => $userId,
'action' => 'login',
'ip' => $request->ip(),
'user_agent' => $request->header('User-Agent'),
'timestamp' => time()
]);

3. Redis存储黑名单

1
2
3
4
5
6
7
// config/cache.php 配置Redis
'jwt_blacklist' => [
'type' => 'redis',
'host' => '127.0.0.1',
'port' => 6379,
'prefix' => 'jwt_blacklist:',
];

安全注意事项

  1. 密钥安全:JWT密钥必须保密,定期更换
  2. HTTPS传输:生产环境必须使用HTTPS
  3. Token存储:前端避免将Token存储在localStorage,推荐使用httpOnly Cookie
  4. 过期时间:合理设置Token过期时间,平衡安全性和用户体验
  5. 黑名单机制:实现Token黑名单,支持强制下线

总结

本文详细介绍了ThinkPHP6中JWT认证中间件的实现方案,包括:

  • JWT扩展包的安装和配置
  • 自定义认证中间件的开发
  • 无痛刷新Token机制的实现
  • 前端集成方案和最佳实践
  • 高级特性和性能优化

通过这套完整的JWT认证方案,可以为API应用提供安全、高效的用户认证机制,同时保证良好的用户体验。在实际项目中,还需要根据具体需求进行调整和优化。

本站由 提供部署服务