Laravel源码解析之Cookie

为了安全起见,Laravel 框架创建的所有 Cookie 都经过加密并使用一个认证码进行签名,这意味着如果客户端修改了它们则需要对其进行有效性验证。我们使用 Illuminate\Http\Request 实例的 cookie 方法从请求中获取 Cookie 的值:

代码语言:javascript
复制
$value = $request->cookie('name');

也可以使用Facade Cookie来读取Cookie的值:

代码语言:javascript
复制
Cookie::get('name', '');//第二个参数的意思是读取不到name的cookie值的话,返回空字符串

添加Cookie到响应

可以使用 响应对象的 cookie 方法将一个 Cookie 添加到返回的 Illuminate\Http\Response 实例中,你需要传递 Cookie 的名称、值、以及有效期(分钟)到这个方法:

代码语言:javascript
复制
return response('Learn Laravel Kernel')->cookie(
    'cookie-name', 'cookie-value', $minutes
);

响应对象的 cookie 方法接收的参数和 PHP 原生函数 setcookie 的参数一致:

代码语言:javascript
复制
return response('Learn Laravel Kernel')->cookie(
    'cookie-name', 'cookie-value', $minutes, $path, $domain, $secure, $httpOnly
);

还可使用Facade Cookiequeue方法以队列的形式将Cookie添加到响应:

代码语言:javascript
复制
Cookie::queue('cookie-name', 'cookie-value');

queue 方法接收 Cookie 实例或创建 Cookie 所必要的参数作为参数,这些 Cookie 会在响应被发送到浏览器之前添加到响应中。

接下来我们来分析一下Laravel中Cookie服务的实现原理。

Cookie服务注册

之前在讲服务提供器的文章里我们提到过,Laravel在BootStrap阶段会通过服务提供器将框架中涉及到的所有服务注册到服务容器里,这样在用到具体某个服务时才能从服务容器中解析出服务来,所以 Cookie服务的注册也不例外,在 config/app.php中我们能找到Cookie对应的服务提供器和门面。

代码语言:javascript
复制
'providers' => [
/*
 * Laravel Framework Service Providers...
 */
......
Illuminate\Cookie\CookieServiceProvider::class,
......

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

Cookie服务的服务提供器是 Illuminate\Cookie\CookieServiceProvider ,其源码如下:

代码语言:javascript
复制
<?php

namespace Illuminate\Cookie;

use Illuminate\Support\ServiceProvider;

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

        return (new CookieJar)-&gt;setDefaultPathAndDomain(
            $config[&#39;path&#39;], $config[&#39;domain&#39;], $config[&#39;secure&#39;], $config[&#39;same_site&#39;] ?? null
        );
    });
}

}

CookieServiceProvider里将 \Illuminate\Cookie\CookieJar类的对象注册为Cookie服务,在实例化时会从Laravel的 config/session.php配置中读取出 pathdomainsecure这些参数来设置Cookie服务用的默认路径和域名等参数,我们来看一下 CookieJarsetDefaultPathAndDomain的实现:

代码语言:javascript
复制
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-&gt;path, this->domain, this-&gt;secure, this->sameSite) = [path, domain, secure, sameSite];

    return $this;
}

}

它只是把这些默认参数保存到 CookieJar对象的属性中,等到 make生成 \Symfony\Component\HttpFoundation\Cookie对象时才会使用它们。

生成Cookie

上面说了生成Cookie用的是 Response对象的 cookie方法, Response的是利用Laravel的全局函数 cookie来生成Cookie对象然后设置到响应头里的,有点乱我们来看一下源码

代码语言:javascript
复制
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) &amp;&amp; function_exists(&#39;cookie&#39;)) {
        $cookie = call_user_func_array(&#39;cookie&#39;, func_get_args());
    }

    $this-&gt;headers-&gt;setCookie($cookie);

    return $this;
}

}

看一下全局函数 cookie的实现:

代码语言:javascript
复制
/**

  • 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-&gt;make(name, value, minutes, path, domain, secure, httpOnly, raw, sameSite);
    }

通过 cookie函数的@return标注我们能知道它返回的是一个 Illuminate\Cookie\CookieJar对象或者是 \Symfony\Component\HttpFoundation\Cookie对象。既 cookie函数在参数 name为空时返回一个 CookieJar对象,否则调用 CookieJarmake方法返回一个 \Symfony\Component\HttpFoundation\Cookie对象。

拿到 Cookie对象后程序接着流程往下走把Cookie设置到 Response对象的 headers属性里,`headers`属性引用了 \Symfony\Component\HttpFoundation\ResponseHeaderBag对象

代码语言:javascript
复制
class ResponseHeaderBag extends HeaderBag
{
public function setCookie(Cookie $cookie)
{
this-&gt;cookies[cookie->getDomain()][cookie-&gt;getPath()][cookie->getName()] = $cookie;
$this->headerNames['set-cookie'] = 'Set-Cookie';
}
}

我们可以看到这里只是把 Cookie对象暂存到了 headers对象里,真正把Cookie发送到浏览器是在 Laravel返回响应时发生的,在 Laravelpublic/index.php里:

代码语言:javascript
复制
$response->send();

Laravel的 Response继承自Symfony的 Responsesend方法定义在 SymfonyResponse

代码语言:javascript
复制
namespace Symfony\Component\HttpFoundation;

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

    if (function_exists(&#39;fastcgi_finish_request&#39;)) {
        fastcgi_finish_request();
    } elseif (!\in_array(PHP_SAPI, array(&#39;cli&#39;, &#39;phpdbg&#39;), 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-&gt;headers-&gt;allPreserveCase() as $name =&gt; $values) {
        foreach ($values as $value) {
            header($name.&#39;: &#39;.$value, false, $this-&gt;statusCode);
        }
    }

    // status
    header(sprintf(&#39;HTTP/%s %s %s&#39;, $this-&gt;version, $this-&gt;statusCode, $this-&gt;statusText), true, $this-&gt;statusCode);

    return $this;
}

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

    return $headers;
}

public function all()
{
    $headers = parent::all();
    foreach ($this-&gt;getCookies() as $cookie) {
        $headers[&#39;set-cookie&#39;][] = (string) $cookie;
    }

    return $headers;
}

}

Responsesend方法里发送响应头时将Cookie数据设置到了Http响应首部的 Set-Cookie字段里,这样当响应发送给浏览器后浏览器就能保存这些Cookie数据了。

至于用门面 Cookie::queue以队列的形式设置Cookie其实也是将Cookie暂存到了 CookieJar对象的 queued属性里

代码语言:javascript
复制
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-&gt;queued[$cookie-&gt;getName()] = $cookie;
}

public function queued($key, $default = null)
{
    return Arr::get($this-&gt;queued, $key, $default);
}

}

然后在 web中间件组里边有一个 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse中间件,它在响应返回给客户端之前将暂存在 queued属性里的Cookie设置到了响应的 headers对象里:

代码语言:javascript
复制
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-&gt;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-&gt;cookies-&gt;getQueuedCookies() as $cookie) {
        $response-&gt;headers-&gt;setCookie($cookie);
    }

    return $response;
}</code></pre></div></div><p>这样在 <code>Response</code>对象调用 <code>send</code>方法时也会把通过 <code>Cookie::queue()</code>设置的Cookie数据设置到 <code>Set-Cookie</code>响应首部中去了。</p><h4 id="ddjor" name="%E8%AF%BB%E5%8F%96Cookie">读取Cookie</h4><p>Laravel读取请求中的Cookie值 <code>$value=$request-&gt;cookie(&#39;name&#39;);</code> 其实是Laravel的 <code>Request</code>对象直接去读取 <code>Symfony</code>请求对象的 <code>cookies</code>来实现的, 我们在写 <code>LaravelRequest</code>对象的文章里有提到它依赖于 <code>Symfony</code>的 <code>Request</code>, <code>Symfony</code>的 <code>Request</code>在实例化时会把PHP里那些 <code>$_POST</code>、 <code>$_COOKIE</code>全局变量抽象成了具体对象存储在了对应的属性中。</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">namespace Illuminate\Http;

class Request extends SymfonyRequest implements Arrayable, ArrayAccess
{
public function cookie(key = null, default = null)
{
return this-&gt;retrieveItem(&#39;cookies&#39;, key, $default);
}

protected function retrieveItem($source, $key, $default)
{
    if (is_null($key)) {
        return $this-&gt;$source-&gt;all();
    }
    //从Request的cookies属性中获取数据
    return $this-&gt;$source-&gt;get($key, $default);
}

}

关于通过门面 Cookie::get()读取Cookie的实现我们可以看下`Cookie`门面源码的实现,通过源码我们知道门面 Cookie除了通过外观模式代理 Cookie服务外自己也定义了两个方法:

代码语言:javascript
复制
<?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[&#39;request&#39;]-&gt;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[&#39;request&#39;]-&gt;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是Laravel web中间件组里的一个中间件,如果想让客户端的Javascript程序能够读Laravel设置的Cookie则需要在 App\Http\Middleware\EncryptCookies$exception里对Cookie名称进行声明。

Laravel中Cookie模块大致的实现原理就梳理完了,希望大家看了我的源码分析后能够清楚Laravel Cookie实现的基本流程这样在遇到困惑或者无法通过文档找到解决方案时可以通过阅读源码看看它的实现机制再相应的设计解决方案。