怎样在PHP项目中实现Token管理?

wen java案例 6

本文目录导读:

怎样在PHP项目中实现Token管理?

  1. 使用JWT(JSON Web Token)方案
  2. 使用框架内置功能(以Laravel为例)
  3. 简单Token实现(不依赖库)
  4. 中间件实现
  5. 完整示例:用户登录系统
  6. 最佳实践建议

我来详细介绍在PHP项目中实现Token管理的几种常见方案。

使用JWT(JSON Web Token)方案

1 安装JWT库

composer require firebase/php-jwt

2 创建Token管理类

<?php
namespace App\Services;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
class TokenService
{
    private string $secretKey;
    private string $algorithm;
    private int $accessTokenExpiration;
    private int $refreshTokenExpiration;
    public function __construct()
    {
        $this->secretKey = getenv('JWT_SECRET') ?: 'your-secret-key-here';
        $this->algorithm = 'HS256';
        $this->accessTokenExpiration = 3600; // 1小时
        $this->refreshTokenExpiration = 604800; // 7天
    }
    /**
     * 生成访问令牌
     */
    public function generateAccessToken(array $payload): string
    {
        $tokenPayload = [
            'iss' => 'your-app-name', // 签发者
            'iat' => time(),           // 签发时间
            'exp' => time() + $this->accessTokenExpiration, // 过期时间
            'sub' => $payload['user_id'],
            'role' => $payload['role'] ?? 'user',
            'type' => 'access'
        ];
        return JWT::encode($tokenPayload, $this->secretKey, $this->algorithm);
    }
    /**
     * 生成刷新令牌
     */
    public function generateRefreshToken(array $payload): string
    {
        $tokenPayload = [
            'iss' => 'your-app-name',
            'iat' => time(),
            'exp' => time() + $this->refreshTokenExpiration,
            'sub' => $payload['user_id'],
            'token_id' => uniqid('refresh_', true),
            'type' => 'refresh'
        ];
        return JWT::encode($tokenPayload, $this->secretKey, $this->algorithm);
    }
    /**
     * 验证令牌
     */
    public function validateToken(string $token): ?object
    {
        try {
            $decoded = JWT::decode($token, new Key($this->secretKey, $this->algorithm));
            return $decoded;
        } catch (ExpiredException $e) {
            // 令牌过期
            return null;
        } catch (\Exception $e) {
            // 其他验证错误
            return null;
        }
    }
}

使用框架内置功能(以Laravel为例)

1 Laravel Sanctum(适合API)

<?php
namespace App\Http\Controllers\Api;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
    /**
     * 用户登录并生成令牌
     */
    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);
        $user = User::where('email', $request->email)->first();
        if (!$user || !Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['提供的凭据不正确。'],
            ]);
        }
        // 创建令牌
        $token = $user->createToken('api-token')->plainTextToken;
        return response()->json([
            'access_token' => $token,
            'token_type' => 'Bearer',
        ]);
    }
    /**
     * 验证用户令牌
     */
    public function user(Request $request)
    {
        return $request->user();
    }
    /**
     * 撤销当前令牌
     */
    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => '已成功注销']);
    }
}

2 路由配置

// routes/api.php
use App\Http\Controllers\Api\AuthController;
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', [AuthController::class, 'user']);
    Route::post('/logout', [AuthController::class, 'logout']);
});

简单Token实现(不依赖库)

<?php
namespace App\Services;
class SimpleTokenService
{
    private string $secretKey;
    private int $expiration;
    public function __construct()
    {
        $this->secretKey = 'your-secret-key';
        $this->expiration = 3600; // 1小时
    }
    /**
     * 生成简单令牌
     */
    public function generateToken(array $data): string
    {
        $payload = base64_encode(json_encode([
            'data' => $data,
            'exp' => time() + $this->expiration,
            'iat' => time()
        ]));
        $signature = hash_hmac('sha256', $payload, $this->secretKey);
        return base64_encode($payload . '.' . $signature);
    }
    /**
     * 验证简单令牌
     */
    public function validateToken(string $token): ?array
    {
        try {
            $decoded = base64_decode($token);
            $parts = explode('.', $decoded);
            if (count($parts) !== 2) {
                return null;
            }
            [$payload, $signature] = $parts;
            // 验证签名
            $expectedSignature = hash_hmac('sha256', $payload, $this->secretKey);
            if (!hash_equals($expectedSignature, $signature)) {
                return null;
            }
            // 解码载荷
            $data = json_decode(base64_decode($payload), true);
            // 检查过期时间
            if ($data['exp'] < time()) {
                return null;
            }
            return $data['data'];
        } catch (\Exception $e) {
            return null;
        }
    }
}

中间件实现

1 认证中间件

<?php
namespace App\Http\Middleware;
use App\Services\TokenService;
use Closure;
use Illuminate\Http\Request;
class TokenAuthMiddleware
{
    private TokenService $tokenService;
    public function __construct(TokenService $tokenService)
    {
        $this->tokenService = $tokenService;
    }
    /**
     * 处理传入请求
     */
    public function handle(Request $request, Closure $next)
    {
        $token = $request->bearerToken();
        if (!$token) {
            return response()->json([
                'error' => '未提供令牌',
                'message' => '请求已禁止'
            ], 401);
        }
        $decoded = $this->tokenService->validateToken($token);
        if (!$decoded) {
            return response()->json([
                'error' => '无效或过期的令牌',
                'message' => '请重新登录'
            ], 401);
        }
        // 将用户信息添加到请求中
        $request->merge(['auth_user' => $decoded]);
        return $next($request);
    }
}

2 刷新令牌中间件

<?php
namespace App\Http\Middleware;
use App\Services\TokenService;
use Closure;
use Illuminate\Http\Request;
class TokenRefreshMiddleware
{
    private TokenService $tokenService;
    public function __construct(TokenService $tokenService)
    {
        $this->tokenService = $tokenService;
    }
    /**
     * 检查令牌是否需要刷新
     */
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);
        // 如果令牌快要过期,生成新的令牌
        if ($request->has('auth_user')) {
            $user = $request->auth_user;
            $tokenExp = $user->exp ?? 0;
            $remainingTime = $tokenExp - time();
            // 剩余时间少于10分钟时刷新令牌
            if ($remainingTime < 600) {
                $newToken = $this->tokenService->generateAccessToken([
                    'user_id' => $user->sub,
                    'role' => $user->role ?? 'user'
                ]);
                $response->headers->set('X-New-Token', $newToken);
            }
        }
        return $response;
    }
}

完整示例:用户登录系统

1 用户模型

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
    protected $fillable = [
        'username',
        'email',
        'password',
        'role'
    ];
    protected $hidden = [
        'password',
        'remember_token',
    ];
}

2 认证控制器

<?php
namespace App\Http\Controllers\Api;
use App\Models\User;
use App\Services\TokenService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
    private TokenService $tokenService;
    public function __construct(TokenService $tokenService)
    {
        $this->tokenService = $tokenService;
    }
    /**
     * 用户注册
     */
    public function register(Request $request)
    {
        $validated = $request->validate([
            'username' => 'required|string|max:255|unique:users',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ]);
        $user = User::create([
            'username' => $validated['username'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);
        // 生成令牌
        $accessToken = $this->tokenService->generateAccessToken([
            'user_id' => $user->id,
            'role' => 'user'
        ]);
        $refreshToken = $this->tokenService->generateRefreshToken([
            'user_id' => $user->id
        ]);
        return response()->json([
            'user' => $user,
            'access_token' => $accessToken,
            'refresh_token' => $refreshToken,
            'token_type' => 'Bearer'
        ], 201);
    }
    /**
     * 用户登录
     */
    public function login(Request $request)
    {
        $validated = $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);
        $user = User::where('email', $validated['email'])->first();
        if (!$user || !Hash::check($validated['password'], $user->password)) {
            return response()->json([
                'error' => '认证失败',
                'message' => '邮箱或密码错误'
            ], 401);
        }
        // 生成令牌
        $accessToken = $this->tokenService->generateAccessToken([
            'user_id' => $user->id,
            'role' => $user->role
        ]);
        $refreshToken = $this->tokenService->generateRefreshToken([
            'user_id' => $user->id
        ]);
        return response()->json([
            'user' => $user,
            'access_token' => $accessToken,
            'refresh_token' => $refreshToken,
            'token_type' => 'Bearer',
            'expires_in' => 3600
        ]);
    }
    /**
     * 刷新令牌
     */
    public function refreshToken(Request $request)
    {
        $validated = $request->validate([
            'refresh_token' => 'required|string'
        ]);
        $decoded = $this->tokenService->validateToken($validated['refresh_token']);
        if (!$decoded || $decoded->type !== 'refresh') {
            return response()->json([
                'error' => '无效的刷新令牌'
            ], 401);
        }
        // 生成新的访问令牌
        $user = User::find($decoded->sub);
        if (!$user) {
            return response()->json([
                'error' => '用户不存在'
            ], 401);
        }
        $newAccessToken = $this->tokenService->generateAccessToken([
            'user_id' => $user->id,
            'role' => $user->role
        ]);
        return response()->json([
            'access_token' => $newAccessToken,
            'token_type' => 'Bearer',
            'expires_in' => 3600
        ]);
    }
}

最佳实践建议

安全性建议

  • 使用HTTPS:始终在HTTPS下传输令牌
  • 令牌有效期:访问令牌短时(15分钟-1小时),刷新令牌长时(7-30天)
  • 密钥管理:密钥存储在环境变量中,定期更换
  • 令牌撤销:实现令牌黑名单机制

性能优化

  • 缓存令牌验证:使用Redis缓存已验证的令牌
  • 数据库索引:为令牌相关字段创建索引
  • 批量操作:需要撤销多个令牌时使用批量操作

常见问题处理

// 令牌失效处理
try {
    $user = auth()->userOrFail();
} catch (\Tymon\JWTAuth\Exceptions\UserNotDefinedException $e) {
    return response()->json(['error' => '用户未找到'], 401);
} catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
    return response()->json(['error' => '令牌已过期'], 401);
} catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
    return response()->json(['error' => '无效令牌'], 401);
}

选择哪种实现方式取决于你的项目需求:

  • 小型项目:使用简单的自定义实现
  • 大型企业应用:推荐使用JWT + 框架内置功能
  • 微服务架构:考虑使用OAuth2.0协议

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