Laravel Sanctum结合SQL Server生成Token时日期转换异常问题
使用Laravel Sanctum实现登录认证,调用createToken生成Token时触发如下SQL错误:
SQLSTATE[22007]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Converting an nvarchar data type to a datetime data type resulted in an out-of-range value. (Connection: sqlsrv, SQL: insert into [personal_access_tokens] ([name], [token], [abilities], [expires_at], [tokenable_id], [tokenable_type], [updated_at], [created_at]) values (insomnia, e1984b68e31dfba8268194d6f7190c65e2702904c4759084634086f970cc9830, ["*"], ?, 1, App\Models\User, 2024-09-24 12:12:03.047, 2024-09-24 12:12:03.047))
但直接执行原生SQL或通过DB::raw插入数据时却能正常运行:
insert into [personal_access_tokens] ([name], [token], [abilities], [expires_at], [tokenable_id], [tokenable_type], [updated_at], [created_at])
values ('insominia', 'd6f25421129a1ad0d05a82d83ded92859d4209272981f46c3b5377503dbfd312', '["*"]', CONVERT(datetime, '2050-02-23 06:20:24.000', 121), 1, 'App\Models\User', CONVERT(datetime, '2016-02-23 06:20:24.000', 121), CONVERT(datetime, '2016-02-23 06:20:24.000', 121))
DB::raw("insert into [personal_access_tokens] ([name], [token], [abilities], [expires_at], [tokenable_id], [tokenable_type], [updated_at], [created_at])
values ('insominia', 'd6f25421129a1ad0d05a82d83ded92859d4209272981f46c3b5377503dbfd312', '["*"]', CONVERT(datetime, '2050-02-23 06:20:24.000', 121), 1, 'App\Models\User', CONVERT(datetime, '2016-02-23 06:20:24.000', 121), CONVERT(datetime, '2016-02-23 06:20:24.000', 121))")
当前使用的登录函数代码:
public function login(Request $request) { $user = User::where('email', $request->email)->first(); $token = $user->createToken($request->device_name)->plainTextToken; return response()->json([ 'token' => $token, 'user' => $user ]); }
问题原因
错误日志中expires_at的占位符为?,说明Laravel在绑定日期参数时,未生成SQL Server兼容的datetime格式或范围值。SQL Server的datetime类型支持的日期范围是1753-01-01 到 9999-12-31,若Sanctum默认生成的过期时间超出该范围,或日期格式不匹配,就会触发转换错误。
解决方案
1. 显式指定Token过期时间(兼容datetime范围)
调用createToken时,手动设置一个在SQL Server datetime范围内的过期时间,例如20年后:
use Carbon\Carbon; public function login(Request $request) { $user = User::where('email', $request->email)->first(); // 显式设置过期时间,确保在SQL Server datetime范围内 $token = $user->createToken( $request->device_name, ['*'], Carbon::now()->addYears(20) )->plainTextToken; return response()->json([ 'token' => $token, 'user' => $user ]); }
2. 修改表字段类型为datetime2
SQL Server的datetime2类型支持更大的日期范围(0001-01-01 到 9999-12-31),与Laravel的Carbon日期兼容性更好。修改personal_access_tokens表的迁移文件:
// 原代码 $table->datetime('expires_at')->nullable(); // 修改为 $table->datetime2('expires_at')->nullable();
执行迁移刷新命令更新表结构:
php artisan migrate:refresh
3. 重写Sanctum的createToken方法处理日期格式
在User模型中重写HasApiTokens trait的createToken方法,通过DB::raw手动转换日期格式:
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\NewAccessToken; use Illuminate\Support\Str; use Carbon\Carbon; use Illuminate\Support\Facades\DB; class User extends Model { use HasApiTokens; public function createToken(string $name, array $abilities = ['*'], ?Carbon $expiresAt = null) { $plainTextToken = Str::random(40); $token = $this->tokens()->create([ 'name' => $name, 'token' => hash('sha256', $plainTextToken), 'abilities' => $abilities, 'expires_at' => $expiresAt ? DB::raw("CONVERT(datetime2, '{$expiresAt->format('Y-m-d H:i:s.u')}', 121)") : null, ]); return new NewAccessToken($token, $plainTextToken); } }
内容的提问来源于stack exchange,提问作者Edinho Rodrigues

