在现代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配置
执行以下命令生成配置文件:
此命令会生成 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
return [ 'secret' => env('JWT_SECRET'), 'algo' => 'HS256', 'ttl' => 7200, '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;
class JWTAuth extends BaseJWTAuth {
public function handle($request, \Closure $next): Response { $token = null; try { $payload = $this->auth->auth(); } catch (TokenExpiredException $e) { 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()); } $request->uid = $payload['uid']->getValue(); $response = $next($request); if (isset($token)) { $this->setAuthentication($response, $token); } return $response; }
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 {
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' => '用户名或密码错误' ]); } $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 ] ] ]); }
public function logout(Request $request): Response { try { JWTAuth::invalidate(); return json([ 'code' => 200, 'message' => '登出成功' ]); } catch (\Exception $e) { return json([ 'code' => 500, 'message' => '登出失败:' . $e->getMessage() ]); } }
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
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
| 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 => { const newToken = response.headers.authorization; if (newToken) { localStorage.setItem('token', newToken); } return response; }, error => { if (error.response && error.response.headers.authorization) { localStorage.setItem('token', error.response.headers.authorization); } 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
| 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);
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 || '登录失败' }; } };
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
namespace app\service;
use think\facade\Cache; use thans\jwt\facade\JWTAuth;
class TokenService {
public function limitUserDevices(int $userId, string $token, int $maxDevices = 3): void { $cacheKey = "user_tokens:{$userId}"; $userTokens = Cache::get($cacheKey, []); $userTokens[] = $token; if (count($userTokens) > $maxDevices) { $oldTokens = array_slice($userTokens, 0, -$maxDevices); foreach ($oldTokens as $oldToken) { try { JWTAuth::setToken($oldToken)->invalidate(); } catch (\Exception $e) { } } $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
namespace app\middleware;
use think\Request; use think\Response; use think\exception\HttpException;
class PermissionCheck {
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); }
private function checkPermission(int $userId, string $permission): bool { return true; } }
|
性能优化建议
1. 缓存用户信息
1 2 3 4
| $userInfo = Cache::remember("user_info:{$userId}", function() use ($userId) { return User::find($userId); }, 300);
|
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
| 'jwt_blacklist' => [ 'type' => 'redis', 'host' => '127.0.0.1', 'port' => 6379, 'prefix' => 'jwt_blacklist:', ];
|
安全注意事项
- 密钥安全:JWT密钥必须保密,定期更换
- HTTPS传输:生产环境必须使用HTTPS
- Token存储:前端避免将Token存储在localStorage,推荐使用httpOnly Cookie
- 过期时间:合理设置Token过期时间,平衡安全性和用户体验
- 黑名单机制:实现Token黑名单,支持强制下线
总结
本文详细介绍了ThinkPHP6中JWT认证中间件的实现方案,包括:
- JWT扩展包的安装和配置
- 自定义认证中间件的开发
- 无痛刷新Token机制的实现
- 前端集成方案和最佳实践
- 高级特性和性能优化
通过这套完整的JWT认证方案,可以为API应用提供安全、高效的用户认证机制,同时保证良好的用户体验。在实际项目中,还需要根据具体需求进行调整和优化。