Laravel核心解讀--Cookie源碼分析

Laravel Cookie源碼分析

使用Cookie的方法

爲了安全起見,Laravel 框架建立的全部 Cookie 都通過加密並使用一個認證碼進行簽名,這意味着若是客戶端修改了它們則須要對其進行有效性驗證。咱們使用 Illuminate\Http\Request 實例的 cookie 方法從請求中獲取 Cookie 的值:php

$value = $request->cookie('name');

也可使用Facade Cookie來讀取Cookie的值:git

Cookie::get('name', '');//第二個參數的意思是讀取不到name的cookie值的話,返回空字符串

添加Cookie到響應github

可使用 響應對象的cookie 方法將一個 Cookie 添加到返回的 Illuminate\Http\Response 實例中,你須要傳遞 Cookie 的名稱、值、以及有效期(分鐘)到這個方法:web

return response('Learn Laravel Kernel')->cookie(
    'cookie-name', 'cookie-value', $minutes
);

響應對象的cookie 方法接收的參數和 PHP 原生函數 setcookie 的參數一致:api

return response('Learn Laravel Kernel')->cookie(
    'cookie-name', 'cookie-value', $minutes, $path, $domain, $secure, $httpOnly
);

還可以使用Facade Cookiequeue方法以隊列的形式將Cookie添加到響應:瀏覽器

Cookie::queue('cookie-name', 'cookie-value');

queue 方法接收 Cookie 實例或建立 Cookie 所必要的參數做爲參數,這些 Cookie 會在響應被髮送到瀏覽器以前添加到響應中。安全

接下來咱們來分析一下Laravel中Cookie服務的實現原理。cookie

Cookie服務註冊

以前在講服務提供器的文章裏咱們提到過,Laravel在BootStrap階段會經過服務提供器將框架中涉及到的全部服務註冊到服務容器裏,這樣在用到具體某個服務時才能從服務容器中解析出服務來,因此Cookie服務的註冊也不例外,在config/app.php中咱們能找到Cookie對應的服務提供器和門面。session

'providers' => [

    /*
     * Laravel Framework Service Providers...
     */
    ......
    Illuminate\Cookie\CookieServiceProvider::class,
    ......
]    

'aliases' => [
    ......
    'Cookie' => Illuminate\Support\Facades\Cookie::class,
    ......
]

Cookie服務的服務提供器是 Illuminate\Cookie\CookieServiceProvider ,其源碼以下:app

<?php

namespace Illuminate\Cookie;

use Illuminate\Support\ServiceProvider;

class CookieServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('cookie', function ($app) {
            $config = $app->make('config')->get('session');

            return (new CookieJar)->setDefaultPathAndDomain(
                $config['path'], $config['domain'], $config['secure'], $config['same_site'] ?? null
            );
        });
    }
}

CookieServiceProvider裏將\Illuminate\Cookie\CookieJar類的對象註冊爲Cookie服務,在實例化時會從Laravel的config/session.php配置中讀取出pathdomainsecure這些參數來設置Cookie服務用的默認路徑和域名等參數,咱們來看一下CookieJarsetDefaultPathAndDomain的實現:

namespace Illuminate\Cookie;

class CookieJar implements JarContract
{
    /**
     * 設置Cookie的默認路徑和Domain
     *
     * @param  string  $path
     * @param  string  $domain
     * @param  bool    $secure
     * @param  string  $sameSite
     * @return $this
     */
    public function setDefaultPathAndDomain($path, $domain, $secure = false, $sameSite = null)
    {
        list($this->path, $this->domain, $this->secure, $this->sameSite) = [$path, $domain, $secure, $sameSite];

        return $this;
    }
}

它只是把這些默認參數保存到CookieJar對象的屬性中,等到make生成\Symfony\Component\HttpFoundation\Cookie對象時纔會使用它們。

生成Cookie

上面說了生成Cookie用的是Response對象的cookie方法,Response的是利用Laravel的全局函數cookie來生成Cookie對象而後設置到響應頭裏的,有點亂咱們來看一下源碼

class Response extends BaseResponse
{
    /**
     * Add a cookie to the response.
     *
     * @param  \Symfony\Component\HttpFoundation\Cookie|mixed  $cookie
     * @return $this
     */
    public function cookie($cookie)
    {
        return call_user_func_array([$this, 'withCookie'], func_get_args());
    }

    /**
     * Add a cookie to the response.
     *
     * @param  \Symfony\Component\HttpFoundation\Cookie|mixed  $cookie
     * @return $this
     */
    public function withCookie($cookie)
    {
        if (is_string($cookie) && function_exists('cookie')) {
            $cookie = call_user_func_array('cookie', func_get_args());
        }

        $this->headers->setCookie($cookie);

        return $this;
    }
}

看一下全局函數cookie的實現:

/**
 * Create a new cookie instance.
 *
 * @param  string  $name
 * @param  string  $value
 * @param  int  $minutes
 * @param  string  $path
 * @param  string  $domain
 * @param  bool  $secure
 * @param  bool  $httpOnly
 * @param  bool  $raw
 * @param  string|null  $sameSite
 * @return \Illuminate\Cookie\CookieJar|\Symfony\Component\HttpFoundation\Cookie
 */
function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain = null, $secure = false, $httpOnly = true, $raw = false, $sameSite = null)
{
    $cookie = app(CookieFactory::class);

    if (is_null($name)) {
        return $cookie;
    }

    return $cookie->make($name, $value, $minutes, $path, $domain, $secure, $httpOnly, $raw, $sameSite);
}

經過cookie函數的@return標註咱們能知道它返回的是一個Illuminate\Cookie\CookieJar對象或者是\Symfony\Component\HttpFoundation\Cookie對象。既cookie函數在無接受參數時返回一個CookieJar對象,在有Cookie參數時調用了CookieJarmake方法返回一個\Symfony\Component\HttpFoundation\Cookie對象。

拿到Cookie對象後程序接着流程往下走把Cookie設置到Response對象的headers屬性裏,`headers`屬性引用了\Symfony\Component\HttpFoundation\ResponseHeaderBag對象

class ResponseHeaderBag extends HeaderBag
{
    public function setCookie(Cookie $cookie)
    {
        $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
        $this->headerNames['set-cookie'] = 'Set-Cookie';
    }
}

咱們能夠看到這裏只是把Cookie對象暫存到了headers對象裏,真正把Cookie發送到瀏覽器是在Laravel返回響應時發生的,在Laravelpublic/index.php裏:

$response->send();

Laravel的Response繼承自Symfony的Responsesend方法定義在SymfonyResponse

namespace Symfony\Component\HttpFoundation;

class Response
{
    /**
     * Sends HTTP headers and content.
     *
     * @return $this
     */
    public function send()
    {
        $this->sendHeaders();
        $this->sendContent();

        if (function_exists('fastcgi_finish_request')) {
            fastcgi_finish_request();
        } elseif (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) {
            static::closeOutputBuffers(0, true);
        }

        return $this;
    }
    
    public function sendHeaders()
    {
        // headers have already been sent by the developer
        if (headers_sent()) {
            return $this;
        }

        // headers
        foreach ($this->headers->allPreserveCase() as $name => $values) {
            foreach ($values as $value) {
                header($name.': '.$value, false, $this->statusCode);
            }
        }

        // status
        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);

        return $this;
    }
    
    /**
     * Returns the headers, with original capitalizations.
     *
     * @return array An array of headers
     */
    public function allPreserveCase()
    {
        $headers = array();
        foreach ($this->all() as $name => $value) {
            $headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value;
        }

        return $headers;
    }
    
    public function all()
    {
        $headers = parent::all();
        foreach ($this->getCookies() as $cookie) {
            $headers['set-cookie'][] = (string) $cookie;
        }

        return $headers;
    }
}

Responsesend方法裏發送響應頭時將Cookie數據設置到了Http響應首部的Set-Cookie字段裏,這樣當響應發送給瀏覽器後瀏覽器就能保存這些Cookie數據了。

至於用門面Cookie::queue以隊列的形式設置Cookie其實也是將Cookie暫存到了CookieJar對象的queued屬性裏

namespace Illuminate\Cookie;
class CookieJar implements JarContract
{
    public function queue(...$parameters)
    {
        if (head($parameters) instanceof Cookie) {
            $cookie = head($parameters);
        } else {
            $cookie = call_user_func_array([$this, 'make'], $parameters);
        }

        $this->queued[$cookie->getName()] = $cookie;
    }
    
    public function queued($key, $default = null)
    {
        return Arr::get($this->queued, $key, $default);
    }
}

而後在web中間件組裏邊有一個\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse中間件,它在響應返回給客戶端以前將暫存在queued屬性裏的Cookie設置到了響應的headers對象裏:

namespace Illuminate\Cookie\Middleware;

use Closure;
use Illuminate\Contracts\Cookie\QueueingFactory as CookieJar;

class AddQueuedCookiesToResponse
{
    /**
     * The cookie jar instance.
     *
     * @var \Illuminate\Contracts\Cookie\QueueingFactory
     */
    protected $cookies;

    /**
     * Create a new CookieQueue instance.
     *
     * @param  \Illuminate\Contracts\Cookie\QueueingFactory  $cookies
     * @return void
     */
    public function __construct(CookieJar $cookies)
    {
        $this->cookies = $cookies;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        foreach ($this->cookies->getQueuedCookies() as $cookie) {
            $response->headers->setCookie($cookie);
        }

        return $response;
    }

這樣在Response對象調用send方法時也會把經過Cookie::queue()設置的Cookie數據設置到Set-Cookie響應首部中去了。

讀取Cookie

Laravel讀取請求中的Cookie值$value = $request->cookie('name'); 實際上是Laravel的Request對象直接去讀取Symfony請求對象的cookies來實現的, 咱們在寫Laravel Request對象的文章裏有提到它依賴於SymfonyRequestSymfonyRequest在實例化時會把PHP裏那些$_POST$_COOKIE全局變量抽象成了具體對象存儲在了對應的屬性中。

namespace Illuminate\Http;

class Request extends SymfonyRequest implements Arrayable, ArrayAccess
{
    public function cookie($key = null, $default = null)
    {
        return $this->retrieveItem('cookies', $key, $default);
    }
    
    protected function retrieveItem($source, $key, $default)
    {
        if (is_null($key)) {
            return $this->$source->all();
        }
        //從Request的cookies屬性中獲取數據
        return $this->$source->get($key, $default);
    }
}

關於經過門面Cookie::get()讀取Cookie的實現咱們能夠看下Cookie門面源碼的實現,經過源碼咱們知道門面Cookie除了經過外觀模式代理Cookie服務外本身也定義了兩個方法:

<?php

namespace Illuminate\Support\Facades;

/**
 * @see \Illuminate\Cookie\CookieJar
 */
class Cookie extends Facade
{
    /**
     * Determine if a cookie exists on the request.
     *
     * @param  string  $key
     * @return bool
     */
    public static function has($key)
    {
        return ! is_null(static::$app['request']->cookie($key, null));
    }

    /**
     * Retrieve a cookie from the request.
     *
     * @param  string  $key
     * @param  mixed   $default
     * @return string
     */
    public static function get($key = null, $default = null)
    {
        return static::$app['request']->cookie($key, $default);
    }

    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'cookie';
    }
}

Cookie::get()Cookie::has()是門面直接讀取Request對象cookies屬性裏的Cookie數據。

Cookie加密

關於對Cookie的加密能夠看一下Illuminate\Cookie\Middleware\EncryptCookies中間件的源碼,它的子類App\Http\Middleware\EncryptCookies是Laravelweb中間件組裏的一箇中間件,若是想讓客戶端的Javascript程序可以讀Laravel設置的Cookie則須要在App\Http\Middleware\EncryptCookies$exception裏對Cookie名稱進行聲明。

Laravel中Cookie模塊大體的實現原理就梳理完了,但願你們看了個人源碼分析後可以清楚Laravel Cookie實現的基本流程這樣在遇到困惑或者沒法經過文檔找到解決方案時能夠經過閱讀源碼看看它的實現機制再相應的設計解決方案。

本文已經收錄在系列文章Laravel源碼學習裏,歡迎訪問閱讀。

相關文章
相關標籤/搜索