编程

如何安全地使用 Laravel Facade

512 2023-12-31 03:06:00

这个问题在 Laravel 文档中也不是特别清晰,不过 Facade 有一件事可能会在系统中导致意外漏洞:

Facade 像单例

与匿名函数绑定的传统服务容器不同,Facade 保留了解析过的实例并在未来 Facade 调用中使用它。我们来看看 Laravel 的代码:

    /**
     * Resolve the facade root instance from the container.
     *
     * @param  string  $name
     * @return mixed
     */
    protected static function resolveFacadeInstance($name)
    {
        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }

        if (static::$app) {
            if (static::$cached) {
                return static::$resolvedInstance[$name] = static::$app[$name];
            }

            return static::$app[$name];
        }
    }

该方法的第一行中,你可以看到它首先检测该 Facade 是否已经有了解析过的实例,如果有,则返回该实例。

如果你像我一样,开始相信无论何时调用 Facade 都会得到该类的新实例,这可能意味着麻烦。

比如,该代码可能很麻烦:

class CreditBalance
{
    /*
     * Scope the credit balance checker to the given user.
     */
    public function forUser(User $user)
    {
        $this->user = $user;
        
        return $this;
    }
    
    /*
     * Get the credit balance of the scoped user,
     * falling back to authenticated user.
     */
    public function getBalance(): int
    {
        $user = $this->user ?? Auth::user();

        return (new CreditBalanceAggregator($user))->balance();
    }
}// This will get the balance of the Auth::user() user,
// because $this->user will be null
$firstBalance = CreditBalance::getBalance();

// And this would get the balance for the given user,
// while also setting the $this->user property.
$secondBalance = CreditBalance::forUser($user)->getBalance();

// Because we have previously set the user on the resolved
// Facade instance, the $this->user property is still set
// and this method will return the balance of the user from
// $this->user property - a different result from our first call.
$thirdBalance = CreditBalance::getBalance();

// $firstBalance != $secondBalance

如你所见,因为 Facade 返回同一个的已解析实例,所以您在 Facade 实例上设置的任何属性都将保留,以便将来调用同一个 Facade。这可能并不总是你想要的行为。

如果你的 Facade 是某种 builder,并且包含的方法旨在将来设置查询范围的某些模型/数据,那么如果没有测试好,上述方法可能会导致严重的错误。

有几种方法可以解决这个问题。

解决方式 #1 - 清除解析过的 Facade 实例

use Illuminate\Support\Facades\Facade;

class CreditBalance
{
    /*
     * Scope the credit balance checker to the given user.
     */
    public function forUser(User $user)
    {
        $this->user = $user;
        
        Facade::clearResolvedInstance('credit-balance');
        
        return $this;
    }
    
    // ...
}

第一个参数应该与你从 getFacadeAccessor() 方法返回的值相同。在我的示例中,它是 'credit-balance' 字符串。

这将在每次你调用 forUser() 方法时清除这个已解析过的 Facade 实例,强制下次 Facade 调用重新解析实例。

解决方式 #2 - 在 scope 方法中返回新实例

use Illuminate\Support\Facades\Facade;

class CreditBalance
{
    public function __construct(User $user = null)
    {
        $this->user = $user;
    }

    /*
     * Scope the credit balance checker to the given user.
     */
    public function forUser(User $user)
    {
        return new static($user);
    }
    
    // ...
}

在这里,当我们想将调用范围扩大到给定用户时,我们不重复使用相同的已解析实例,而是返回该类的一个全新实例。通过这种方式,我们可以对构建器进行干净的重置,并且我们可以相信,在没有作用域用户的情况下,未来对 Facade 的调用将返回我们期望的结果。