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

Laravel 10中添加HashId Cast后无法获取当前登录用户

问题描述

在基于Laravel 10、Laravel Sanctum和React的项目中,为User模型的id和user_id字段配置UserHashId Cast后,出现以下问题:

  • 无法获取当前登录用户
  • 登录后调用获取用户接口时,会话中的用户信息异常
  • 移除该Cast配置后可正常获取登录用户

需求是实现用户ID哈希化,但目前未达预期效果。


相关代码

AuthController.php

public function user(GetUser $getUser, HashIdService $hashIdService)
{
    $user = auth()->user();

    $decodedUserId = $hashIdService->decode($user->id);

    return $getUser($decodedUserId);
}


public function login(LoginRequest $loginRequest, LoginUser $loginUser, HashIdService $hashIdService)
{
    return $loginUser($loginRequest, $hashIdService);
}

GetUser.php

<?php

namespace App\Actions\Users;

use App\Models\User;

class GetUser
{
    public function __invoke($decodedUserId)
    {
        // get user profile based on id
        try {
            $userProfile = User::with('profile', 'role:id,role_name', 'group:id,group_name')
                ->join('user_profiles', 'users.id', '=', 'user_profiles.user_id')
                ->select('users.*', 'user_profiles.status_id', 'user_profiles.updated_by', 'user_profiles.distributor_id', 'user_profiles.operator_id')
                ->where('users.id', $decodedUserId)
                ->first();

            return response()->json([
                'isSuccess' => true,
                'user'      => $userProfile,
            ]);
        } catch (\Throwable $th) {
            //throw $th;
            return response()->json([
                'isSuccess' => false,
                'message'   => $th->getMessage(),
            ], 500);
        }
    }
}

LoginUser.php

<?php

namespace App\Actions\Users;

use App\Http\Requests\Auth\LoginRequest;
use App\Models\User;
use App\Services\HashIdService;
use Illuminate\Support\Facades\Log;

class LoginUser
{
    public function __invoke(LoginRequest $request, HashIdService $hashIdService)
    {
        try {
            $request->authenticate();

            // Use the authenticated user's ID directly
            $userId = auth()->id();

          
            $userProfile = User::with('profile', 'role:id,role_name', 'group:id,group_name')
                ->join('user_profiles', 'users.id', '=', 'user_profiles.user_id')
                ->select('users.*', 'user_profiles.status_id', 'user_profiles.updated_by')
                ->where('users.id', $hashIdService->decode($userId))
                ->first();

            return response()->json($userProfile, 200);
        } catch (\Throwable $th) {
            // Log the exception details
            Log::error($th);

            return response()->json(
                [
                    'isSuccess' => false,
                    'message'   => $th->getMessage(),
                ],
                404,
            );
        }
    }
}

HashIdService.php

<?php

namespace App\Services;

use Hashids\Hashids;

class HashIdService
{
    public function __construct(
        public $hashIds = new Hashids('', 20, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'),
    ) {
    }

    public function encode($id)
    {
        return $this->hashIds->encode($id);
    }

    public function decode($hashId)
    {
        if (is_int($hashId)) {
            return $hashId;
        }

        $decodedIds = $this->hashIds->decode($hashId);

        if (count($decodedIds) > 1) {
            return intval($decodedIds);
        } else {
            return intval($decodedIds[0]);
        }
    }
}

User.php

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;

use App\Casts\UserHashId;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    public function profile()
    {
        return $this->hasOne(UserProfile::class, 'user_id', 'id');
    }

    public function distributor()
    {
        return $this->hasOne(Distributor::class, 'email', 'email');
    }

    public function temporaryPassword()
    {
        return $this->hasOne(TemporaryPasswords::class, 'user_id', 'id');
    }

    public function group()
    {
        return $this->hasOne(UserGroup::class, 'id', 'group_id');
    }

    public function role()
    {
        return $this->hasOne(UserRole::class, 'id', 'role_id');
    }

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = ['name', 'email', 'password', 'role_id', 'group_id'];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = ['password', 'remember_token'];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'id'      => UserHashId::class,
        'user_id' => UserHashId::class,

        'email_verified_at' => 'datetime',
        'password'          => 'hashed',
    ];
}

UserHashId.php

<?php

namespace App\Casts;

use App\Services\UserHashIdService;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class UserHashId implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        // Ensure that the value is not null before encoding
        if ($value !== null) {
            return (new UserHashIdService())->encode($value);
        }

        return $value;
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function set(Model $model, string $key, $value, array $attributes)
    {
        // Ensure that the value is not null before decoding
        if ($value !== null) {
            $decodedValue = (new UserHashIdService())->decode($value);

            return $decodedValue !== $decodedValue ? $decodedValue : $value;
        }

        return $value;
    }
}

api.php

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

Route::group(['middleware' => ['auth:sanctum']], function () {

   Route::get('/user', [AuthController::class, 'user']);

});

问题根源与解决方案

核心问题

  1. Cast破坏框架核心逻辑:对User模型的id字段应用Cast后,Laravel Sanctum认证时会用哈希后的ID去匹配数据库中的原始整数ID,导致会话验证失败,无法获取登录用户。
  2. 哈希服务不统一:UserHashId Cast使用UserHashIdService,业务代码用HashIdService,两个服务的Hashids配置可能不一致,导致编码解码不匹配。
  3. 冗余解码操作:auth()->id()返回的是数据库原始整数ID,无需再解码,当前代码的解码逻辑会引发错误。

修复步骤

1. 调整Cast作用范围,保留原始ID供框架使用

不要对User模型的id字段应用Cast,改用访问器返回哈希ID,框架内部仍使用原始整数ID:
修改User.php:

// 移除$casts中的id和user_id配置(User模型本身没有user_id字段,属于配置错误)
protected $casts = [
    'email_verified_at' => 'datetime',
    'password'          => 'hashed',
];

// 添加访问器返回哈希后的ID
public function getHashIdAttribute()
{
    return app(HashIdService::class)->encode($this->id);
}

如果需要对关联模型(如UserProfile)的user_id字段做哈希处理,可在UserProfile模型中添加Cast,确保仅对外返回哈希值,内部存储用原始ID。

2. 统一哈希服务实例

修改UserHashId.php,确保Cast使用全局的HashIdService:

<?php

namespace App\Casts;

use App\Services\HashIdService;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class UserHashId implements CastsAttributes
{
    protected $hashIdService;

    public function __construct()
    {
        $this->hashIdService = app(HashIdService::class);
    }

    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        if ($value !== null) {
            return $this->hashIdService->encode($value);
        }

        return $value;
    }

    public function set(Model $model, string $key, $value, array $attributes)
    {
        if ($value !== null) {
            // 仅对哈希字符串解码,避免整数ID重复解码
            if (!is_int($value)) {
                $decodedValue = $this->hashIdService->decode($value);
                return $decodedValue;
            }
            return $value;
        }

        return $value;
    }
}

3. 修正业务代码中的ID处理逻辑

修改AuthController.php的user方法:

public function user(GetUser $getUser)
{
    // auth()->id()直接返回原始整数ID,无需解码
    return $getUser(auth()->id());
}

修改LoginUser.php:

public function __invoke(LoginRequest $request)
{
    try {
        $request->authenticate();

        $userProfile = User::with('profile', 'role:id,role_name', 'group:id,group_name')
            ->join('user_profiles', 'users.id', '=', 'user_profiles.user_id')
            ->select('users.*', 'user_profiles.status_id', 'user_profiles.updated_by')
            ->where('users.id', auth()->id())
            ->first();

        // 给返回结果添加哈希ID字段
        if ($userProfile) {
            $userProfile->setAttribute('hash_id', app(HashIdService::class)->encode($userProfile->id));
        }

        return response()->json($userProfile, 200);
    } catch (\Throwable $th) {
        Log::error($th);

        return response()->json(
            [
                'isSuccess' => false,
                'message'   => $th->getMessage(),
            ],
            404,
        );
    }
}

修改GetUser.php:

public function __invoke($userId)
{
    try {
        $userProfile = User::with('profile', 'role:id,role_name', 'group:id,group_name')
            ->join('user_profiles', 'users.id', '=', 'user_profiles.user_id')
            ->select('users.*', 'user_profiles.status_id', 'user_profiles.updated_by', 'user_profiles.distributor_id', 'user_profiles.operator_id')
            ->where('users.id', $userId)
            ->first();

        if ($userProfile) {
            $userProfile->setAttribute('hash_id', app(HashIdService::class)->encode($userProfile->id));
        }

        return response()->json([
            'isSuccess' => true,
            'user'      => $userProfile,
        ]);
    } catch (\Throwable $th) {
        return response()->json([
            'isSuccess' => false,
            'message'   => $th->getMessage(),
        ], 500);
    }
}

4. 清理冗余代码

删除UserHashIdService,确保所有哈希操作都依赖HashIdService,避免服务不一致问题。

关键说明

  • 框架内部的认证、会话管理必须依赖原始整数ID,不能对模型id字段应用Cast,否则会破坏核心逻辑。
  • 哈希ID仅用于对外API响应和接收前端传入的参数,内部数据库操作始终使用原始整数ID。
  • 统一哈希服务实例,避免因配置差异导致的编码解码不匹配。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 06:02:02