You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Laravel中基于jwt-auth生成并验证Refresh Token?

Laravel JWT 实现基于HttpOnly Cookie的Refresh Token静默刷新方案

你当前代码里用auth()->attempt()生成refresh token的方式存在问题——这会生成和access token完全一致的JWT,无法独立管控失效逻辑,不符合refresh token的设计初衷。下面是一套完整的实现方案:

1. 创建Refresh Token数据表

首先生成迁移文件,用来持久化存储用户的refresh token:

php artisan make:migration create_refresh_tokens_table

修改迁移文件内容:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('refresh_tokens', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('token', 100)->unique();
            $table->timestamp('expires_at');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('refresh_tokens');
    }
};

执行迁移:

php artisan migrate

2. 配置模型关联

在User模型中添加与refresh token的关联:

// app/Models/User.php
public function refreshTokens()
{
    return $this->hasMany(RefreshToken::class);
}

创建RefreshToken模型:

php artisan make:model RefreshToken

模型内容如下:

// app/Models/RefreshToken.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class RefreshToken extends Model
{
    protected $fillable = ['token', 'expires_at'];
}

3. 修改Login方法,生成并存储Refresh Token

更新AuthController的login方法,仅在用户提交remember-me为true时生成refresh token:

// app/Http/Controllers/AuthController.php
use Illuminate\Support\Str;
use App\Models\RefreshToken;

public function login(Request $request)
{
    $validator = Validator::make($request->all(), [
        'email' => 'required|email',
        'password' => 'required|string|min:6',
        'remember-me' => 'boolean|nullable',
    ]);

    if ($validator->fails()) {
        return response()->json($validator->errors(), 422);
    }

    if (!$token = auth()->attempt($validator->validated())) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    $response = $this->createNewToken($token);

    // 仅当用户勾选「记住我」时生成refresh token
    if ($request->boolean('remember-me')) {
        // 生成随机字符串作为refresh token
        $refreshToken = Str::random(64);
        $expiresAt = now()->addMonths(6);

        // 存储哈希后的token到数据库(避免明文泄露风险)
        auth()->user()->refreshTokens()->create([
            'token' => hash('sha256', $refreshToken),
            'expires_at' => $expiresAt,
        ]);

        // 设置HttpOnly Cookie
        $cookie = cookie('__refresh-token')
            ->withValue($refreshToken)
            ->withExpires($expiresAt->getTimestamp())
            ->withSecure(config('app.env') === 'production')
            ->withHttpOnly(true)
            ->withSameSite('none');

        $response->cookie($cookie);
    }

    return $response;
}

4. 新增静默刷新接口

在AuthController中添加silentLogin方法,用于通过Cookie中的refresh token获取新的access token:

public function silentLogin(Request $request)
{
    // 从Cookie中获取refresh token
    $refreshToken = $request->cookie('__refresh-token');

    if (!$refreshToken) {
        return response()->json(['error' => 'No refresh token provided'], 401);
    }

    // 哈希后匹配数据库记录
    $hashedToken = hash('sha256', $refreshToken);
    $tokenRecord = RefreshToken::where('token', $hashedToken)->first();

    if (!$tokenRecord || $tokenRecord->expires_at->isPast()) {
        // 清除无效Cookie并返回错误
        $response = response()->json(['error' => 'Invalid or expired refresh token'], 401);
        return $response->withoutCookie('__refresh-token');
    }

    // 登录关联用户并生成新的access token
    auth()->login($tokenRecord->user);
    $newAccessToken = auth()->generateToken();

    // 可选:滚动刷新refresh token,增强安全性
    $newRefreshToken = Str::random(64);
    $newExpiresAt = now()->addMonths(6);

    $tokenRecord->update([
        'token' => hash('sha256', $newRefreshToken),
        'expires_at' => $newExpiresAt,
    ]);

    // 更新Cookie
    $cookie = cookie('__refresh-token')
        ->withValue($newRefreshToken)
        ->withExpires($newExpiresAt->getTimestamp())
        ->withSecure(config('app.env') === 'production')
        ->withHttpOnly(true)
        ->withSameSite('none');

    return $this->createNewToken($newAccessToken)->cookie($cookie);
}

在routes/api.php中添加接口路由:

Route::post('/silent-login', [AuthController::class, 'silentLogin']);

5. 完善Logout方法

更新logout方法,同时清除数据库中的refresh token和前端Cookie:

public function logout()
{
    $refreshToken = request()->cookie('__refresh-token');

    if ($refreshToken) {
        $hashedToken = hash('sha256', $refreshToken);
        RefreshToken::where('token', $hashedToken)->delete();
    }

    auth()->logout();

    return response()->json(['message' => 'User successfully signed out'])
        ->withoutCookie('__refresh-token');
}

前端调用逻辑

  • 用户登录勾选「记住我」后,后端自动设置HttpOnly Cookie,前端无需手动存储refresh token,浏览器会在请求时自动携带。
  • 当access token过期时,前端调用/silent-login接口,浏览器自动带上Cookie中的refresh token,后端验证通过后返回新的access token,实现静默登录。

额外优化

  • 生产环境必须开启Secure Cookie,确保仅通过HTTPS传输。
  • 定期清理数据库中过期的refresh token,可通过任务调度实现:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    $schedule->call(function () {
        RefreshToken::where('expires_at', '<', now())->delete();
    })->daily();
}

内容的提问来源于stack exchange,提问作者Bufer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 22:57:22