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
,其原始碼如下:
<?php
namespace 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
響應首部中去了。
讀取Cookie
Laravel讀取請求中的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
服務外自己也定義了兩個方法:
<?php
namespace 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原始碼學習裡,歡迎訪問閱讀。