Laravel Cookie源码分析使用Cookie的方法为了安全起见,Laravel 框架创建的所有 Cookie 都经过加密并使用一个认证码进行签名,这意味着如果客户端修改了它们则需要对其进行有效性验证。我们使用 IlluminateHttpRequest 实例的 cookie 方法从请求中获取 Cookie 的值:$value = $request->cookie(’name’);也可以使用Facade Cookie来读取Cookie的值:Cookie::get(’name’, ‘’);//第二个参数的意思是读取不到name的cookie值的话,返回空字符串添加Cookie到响应可以使用 响应对象的cookie 方法将一个 Cookie 添加到返回的 IlluminateHttpResponse 实例中,你需要传递 Cookie 的名称、值、以及有效期(分钟)到这个方法:return response(‘Learn Laravel Kernel’)->cookie( ‘cookie-name’, ‘cookie-value’, $minutes);响应对象的cookie 方法接收的参数和 PHP 原生函数 setcookie 的参数一致:return response(‘Learn Laravel Kernel’)->cookie( ‘cookie-name’, ‘cookie-value’, $minutes, $path, $domain, $secure, $httpOnly);还可使用Facade Cookie的queue方法以队列的形式将Cookie添加到响应:Cookie::queue(‘cookie-name’, ‘cookie-value’);queue 方法接收 Cookie 实例或创建 Cookie 所必要的参数作为参数,这些 Cookie 会在响应被发送到浏览器之前添加到响应中。接下来我们来分析一下Laravel中Cookie服务的实现原理。Cookie服务注册之前在讲服务提供器的文章里我们提到过,Laravel在BootStrap阶段会通过服务提供器将框架中涉及到的所有服务注册到服务容器里,这样在用到具体某个服务时才能从服务容器中解析出服务来,所以Cookie服务的注册也不例外,在config/app.php中我们能找到Cookie对应的服务提供器和门面。‘providers’ => [ /* * Laravel Framework Service Providers… / …… IlluminateCookieCookieServiceProvider::class, ……] ‘aliases’ => [ …… ‘Cookie’ => IlluminateSupportFacadesCookie::class, ……]Cookie服务的服务提供器是 IlluminateCookieCookieServiceProvider ,其源码如下:<?phpnamespace IlluminateCookie;use IlluminateSupportServiceProvider;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里将IlluminateCookieCookieJar类的对象注册为Cookie服务,在实例化时会从Laravel的config/session.php配置中读取出path、domain、secure这些参数来设置Cookie服务用的默认路径和域名等参数,我们来看一下CookieJar里setDefaultPathAndDomain的实现:namespace IlluminateCookie;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生成SymfonyComponentHttpFoundationCookie对象时才会使用它们。生成Cookie上面说了生成Cookie用的是Response对象的cookie方法,Response的是利用Laravel的全局函数cookie来生成Cookie对象然后设置到响应头里的,有点乱我们来看一下源码class Response extends BaseResponse{ /* * Add a cookie to the response. * * @param SymfonyComponentHttpFoundationCookie|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 SymfonyComponentHttpFoundationCookie|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 IlluminateCookieCookieJar|SymfonyComponentHttpFoundationCookie /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标注我们能知道它返回的是一个IlluminateCookieCookieJar对象或者是SymfonyComponentHttpFoundationCookie对象。既cookie函数在无接受参数时返回一个CookieJar对象,在有Cookie参数时调用了CookieJar的make方法返回一个SymfonyComponentHttpFoundationCookie对象。拿到Cookie对象后程序接着流程往下走把Cookie设置到Response对象的headers属性里,`headers`属性引用了SymfonyComponentHttpFoundationResponseHeaderBag对象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返回响应时发生的,在Laravel的public/index.php里:$response->send();Laravel的Response继承自Symfony的Response,send方法定义在Symfony的Response里namespace SymfonyComponentHttpFoundation;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; }} 在Response的send方法里发送响应头时将Cookie数据设置到了Http响应首部的Set-Cookie字段里,这样当响应发送给浏览器后浏览器就能保存这些Cookie数据了。至于用门面Cookie::queue以队列的形式设置Cookie其实也是将Cookie暂存到了CookieJar对象的queued属性里namespace IlluminateCookie;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中间件组里边有一个IlluminateCookieMiddlewareAddQueuedCookiesToResponse中间件,它在响应返回给客户端之前将暂存在queued属性里的Cookie设置到了响应的headers对象里:namespace IlluminateCookieMiddleware;use Closure;use IlluminateContractsCookieQueueingFactory as CookieJar;class AddQueuedCookiesToResponse{ /* * The cookie jar instance. * * @var IlluminateContractsCookieQueueingFactory / protected $cookies; /* * Create a new CookieQueue instance. * * @param IlluminateContractsCookieQueueingFactory $cookies * @return void / public function __construct(CookieJar $cookies) { $this->cookies = $cookies; } /* * Handle an incoming request. * * @param IlluminateHttpRequest $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响应首部中去了。读取CookieLaravel读取请求中的Cookie值$value = $request->cookie(’name’); 其实是Laravel的Request对象直接去读取Symfony请求对象的cookies来实现的, 我们在写Laravel Request对象的文章里有提到它依赖于Symfony的Request, Symfony的Request在实例化时会把PHP里那些$_POST、$_COOKIE全局变量抽象成了具体对象存储在了对应的属性中。namespace IlluminateHttp;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服务外自己也定义了两个方法:<?phpnamespace IlluminateSupportFacades;/* * @see IlluminateCookieCookieJar /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的加密可以看一下IlluminateCookieMiddlewareEncryptCookies中间件的源码,它的子类AppHttpMiddlewareEncryptCookies是Laravelweb中间件组里的一个中间件,如果想让客户端的Javascript程序能够读Laravel设置的Cookie则需要在AppHttpMiddlewareEncryptCookies的$exception里对Cookie名称进行声明。Laravel中Cookie模块大致的实现原理就梳理完了,希望大家看了我的源码分析后能够清楚Laravel Cookie实现的基本流程这样在遇到困惑或者无法通过文档找到解决方案时可以通过阅读源码看看它的实现机制再相应的设计解决方案。本文已经收录在系列文章Laravel源码学习里,欢迎访问阅读。
...