Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Developing cacheable PHP applications - ZendCon 2018

Ca901ddcea38854b9783781c91fc87c9?s=47 Thijs Feryn
October 17, 2018

Developing cacheable PHP applications - ZendCon 2018

Slides for my "Developing cacheable PHP applications" presentation at ZendCon 2018 in Las Vegas.

See https://feryn.eu/speaking/developing-cacheable-php-applications-zendcon-2018/ for more information about this presentations

Ca901ddcea38854b9783781c91fc87c9?s=128

Thijs Feryn

October 17, 2018
Tweet

Transcript

  1. Developing cacheable PHP applications Thijs Feryn

  2. Slow websites suck

  3. Hi, I’m Thijs

  4. I’m @ThijsFeryn on Twitter

  5. I’m an Evangelist At

  6. I’m an Evangelist At

  7. None
  8. Cache

  9. Don’t recompute if the data hasn’t changed

  10. None
  11. None
  12. Reverse caching proxy

  13. Normally User Server

  14. With ReCaPro * User ReCaPro Server * Reverse Caching Proxy

  15. Content Delivery Network

  16. None
  17. None
  18. HTTP caching mechanisms Expires: Sat, 09 Sep 2017 14:30:00 GMT

    Cache-control: public, max-age=3600, s-maxage=86400 Cache-control: private, no-cache, no-store
  19. In an ideal world

  20. ✓Stateless ✓Well-defined TTL ✓Cache / no-cache per resource ✓Cache variations

    ✓Conditional requests ✓HTTP fragments for non- cacheable content In an ideal world
  21. Reality sucks

  22. Common problems

  23. None
  24. None
  25. Time To Live

  26. Cache variations

  27. Authentication

  28. Legacy

  29. What if we could design our software with HTTP caching

    in mind?
  30. ✓Portability ✓Developer empowerment ✓Control ✓Consistent caching behavior Caching state of

    mind
  31. None
  32. Twig templates

  33. None
  34. composer require symfony/flex composer require annotations twig translation

  35. <?php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class

    DefaultController extends Controller { /** * @Route("/", name="home") */ public function index() { return $this->render('index.twig'); } } src/Controller/DefaultController.php
  36. {% extends "base.twig" %} {% block title %}Home{% endblock %}

    {% block content %} <div class="page-header"> <h1>{{ 'example' | trans }}</h1> </div> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci eget libero sollicitudin, non ultrices turpis mollis. Aliquam sit amet tempus elit. Ut viverra risus enim, ut venenatis justo accumsan nec. Praesent a dolor tellus. Maecenas non mauris leo. Pellentesque lobortis turpis at dapibus laoreet. Mauris rhoncus nulla et urna mollis, et lobortis magna ornare. Etiam et sapien consequat, egestas felis sit amet, dignissim enim.</p> <p>Quisque quis mollis justo, imperdiet fermentum velit. Aliquam nulla justo, consectetur et diam non, luctus commodo metus. Vestibulum fermentum efficitur nulla non luctus. Nunc lorem nunc, mollis id efficitur et, aliquet sit amet ante. Sed ipsum turpis, vehicula eu semper eu, malesuada eget leo. Vestibulum venenatis dui id pulvinar suscipit. Etiam nec massa pharetra justo pharetra dignissim quis non magna. Integer id convallis lectus. Nam non ullamcorper metus. Ut vestibulum ex ut massa posuere tincidunt. Vestibulum hendrerit neque id lorem rhoncus aliquam. Duis a facilisis metus, a faucibus nulla.</p> {% endblock %} templates/index.twig
  37. <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/html" xmlns:hx="http://purl.org/NET/hinclude"> <head> <title>{% block title %}{%

    endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> </head> <body> <div class="container-fluid"> {{ include('header.twig') }} <div class="row"> <div class="col-sm-3 col-lg-2"> {{ include('nav.twig') }} </div> <div class="col-sm-9 col-lg-10"> {% block content %}{% endblock %} </div> </div> {{ include('footer.twig') }} </div> </body> </html> templates/base.twig
  38. <nav class="navbar navbar-default navbar-fixed-side"> <ul class="nav"> <li><a href="{{ url('home') }}">Home</a></li>

    <li><a href="{{ url('login') }}">{{ 'log_in' | trans }}</a></li> <li><a href="{{ url('private') }}">Private</a></li> </ul> </nav> templates/nav.twig
  39. <footer> <hr /> <small>Footer</small> </footer> templates/footer.twig

  40. home: Home welcome : Welcome to the site rendered :

    Rendered at %date% example : An example page log_in : Log in login : Login log_out : Log out username : Username password : Password private : Private privatetext : Looks like some very private data translations/messages.en.yml
  41. home: Start welcome : Welkom op de site rendered :

    Samengesteld op %date% example : Een voorbeeldpagina log_in : Inloggen login : Login log_out : Uitloggen username : Gebruikersnaam password : Wachtwoord private : Privé privatetext : Deze tekst ziet er vrij privé uit translations/messages.nl.yml
  42. <?php namespace App\EventListener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; class LocaleListener { public function

    onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $preferredLanguage = $request->getPreferredLanguage(); if(null !== $preferredLanguage) { $request->setLocale($preferredLanguage); } } } src/EventListener/LocaleListener.php
  43. services: _defaults: autowire: true autoconfigure: true public: false App\: resource:

    '../src/*' exclude: '../src/{Entity,Migrations,Tests,Kernel.php}' App\Controller\: resource: '../src/Controller' tags: ['controller.service_arguments'] App\EventListener\LocaleListener: tags: - { name: kernel.event_listener, event: kernel.request, priority: 100} config/services.yml
  44. Accept-Language: en Accept-Language: nl VS

  45. None
  46. None
  47. The mission Maximum Cacheability

  48. Cache-control

  49. Cache-Control: public, s-maxage=500

  50. <?php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class

    DefaultController extends Controller { /** * @Route("/", name="home") */ public function index() { return $this ->render('index.twig') ->setSharedMaxAge(500) ->setPublic(); } }
  51. Conditional requests

  52. Only fetch payload that has changed

  53. HTTP/1.1 200 OK

  54. Otherwise: HTTP/1.1 304 Not Modified

  55. Conditional requests HTTP/1.1 200 OK Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27 Content-type:

    text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0
  56. Conditional requests HTTP/1.0 304 Not Modified Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27

    GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0 If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
  57. Conditional requests HTTP/1.1 200 OK Host: localhost Last-Modified: Fri, 22

    Jul 2016 10:11:16 GMT Content-type: text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0
  58. Conditional requests HTTP/1.0 304 Not Modified Host: localhost Last-Modified: Fri,

    22 Jul 2016 10:11:16 GMT GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0 If-Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
  59. composer require symfony-bundles/redis-bundle

  60. <?php namespace App\EventListener; use Symfony\Bridge\Monolog\Logger; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use

    Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use SymfonyBundles\RedisBundle\Redis\Client as RedisClient; class ConditionalRequestListener { protected $redis; protected $logger; public function __construct(RedisClient $redis) { $this->redis = $redis; } protected function isModified(Request $request, $etag) { if ($etags = $request->getETags()) { return in_array($etag, $etags) || in_array('*', $etags); } return true; } ... src/EventListener/ConditionalRequestListener.php
  61. { $this->redis = $redis; $this->logger = $logger; } protected function

    isModified(Request $request, $etag) { if ($etags = $request->getETags()) { return in_array($etag, $etags) || in_array('*', $etags); } return true; } public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $etag = $this->redis->get('etag:'.md5($request->getUri())); if(!$this->isModified($request,$etag)) { $event->setResponse(Response::create('Not Modified',Response::HTTP_NOT_MODIFIED)); } } public function onKernelResponse(FilterResponseEvent $event) { $response = $event->getResponse(); $request = $event->getRequest(); $etag = md5($response->getContent()); $response->setEtag($etag); if($this->isModified($request,$etag)) { $this->redis->set('etag:'.md5($request->getUri()),$etag); } } } src/EventListener/ConditionalRequestListener.php
  62. Do not cache

  63. Cache-Control: private, no-store

  64. /** * @Route("/private", name="private") */ public function private() { $response

    = $this ->render('private.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; } Dot not cache private page
  65. session cookie No cache

  66. None
  67. Code renders single HTTP response

  68. Lowest common denominator: no cache

  69. Block caching

  70. <esi:include src="/header" /> Edge Side Includes ✓Placeholder ✓Parsed by Varnish

    ✓Output is a composition of blocks ✓State per block ✓TTL per block
  71. Surrogate-Capability: key="ESI/1.0" Surrogate-Control: content="ESI/1.0" Varnish Backend <esi:include src="/header" /> Parse

    ESI placeholders Varnish
  72. ESI vs AJAX

  73. ✓ Server-side ✓ Standardized ✓ Processed on the “edge”, no

    in the browser ✓ Generally faster Edge-Side Includes - Sequential - One fails, all fail - Limited implementation in Varnish
  74. ✓ Client-side ✓ Common knowledge ✓ Parallel processing ✓ Graceful

    degradation AJAX - Processed by the browser - Extra roundtrips - Somewhat slower
  75. Subrequests

  76. <div class="container-fluid">
 {{ include('header.twig') }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">


    {{ include('nav.twig') }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ include('footer.twig') }}
 </div> <div class="container-fluid">
 {{ render_esi(url('header')) }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">
 {{ render_esi(url('nav')) }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ render_esi(url('footer')) }}
 </div>
  77. <div class="container-fluid"> <esi:include src="/header" /> <div class="row"> <div class="col-sm-3 col-lg-2">

    <esi:include src="/nav" /> </div> <div class="col-sm-9 col-lg-10"> <div class="page-header"> <h1>An example page <small>Rendered at 2017-05-17 16:57:14</small></h1> </div> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci eget libero sollicitudin,…</p> </div> </div> <esi:include src="/footer" /> </div>
  78. /** * @Route("/", name="home") */ public function index() { return

    $this ->render('index.twig') ->setPublic() ->setSharedMaxAge(500); } /** * @Route("/header", name="header") */ public function header() { $response = $this ->render('header.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; } /** * @Route("/footer", name="footer") */ public function footer() { $response = $this->render('footer.twig'); $response ->setSharedMaxAge(500) ->setPublic(); return $response; } /** * @Route("/nav", name="nav") */ public function nav() { $response = $this->render('nav.twig'); $response ->setVary('X-Login',false) ->setSharedMaxAge(500) ->setPublic(); return $response; } Controller action per fragment
  79. Problem: no language cache variation

  80. Vary: Accept-Language

  81. <?php namespace App\EventListener; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; class VaryListener { public function

    onKernelResponse(FilterResponseEvent $event) { $response = $event->getResponse(); $response->setVary('Accept-Language',false); } } src/EventListener/VaryListener.php
  82. What if you can't leverage HTTP?

  83. Write VCL

  84. sub vcl_recv { if (req.url ~ "^/status\.php$" || req.url ~

    "^/update\.php$" || req.url ~ "^/admin$" || req.url ~ "^/admin/.*$" || req.url ~ "^/flag/.*$" || req.url ~ "^.*/ajax/.*$" || req.url ~ "^.*/ahah/.*$") { return (pass); } } URL blacklist example
  85. vcl 4.0; sub vcl_recv { if (req.http.Cookie) { set req.http.Cookie

    = ";" + req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); set req.http.Cookie = regsuball(req.http.Cookie, ";(SESS[a-z0-9]+|SSESS[a-z0-9]+|NO_CACHE)=", "; \1="); set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); if (req.http.Cookie == "") { unset req.http.Cookie; } else { return (pass); } } } Cookie example
  86. None
  87. Transfer-Encoding: chunked

  88. Auto-placeholdering

  89. BigPipe

  90. <span data-big-pipe-placeholder- id="callback=Drupal%5CCore%5CRender%5CElem ent%5CStatusMessages%3A%3ArenderMessages&a mp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2 PSiJuMntExoshbm1kh2wQzzzAA"></span>

  91. <script type="application/vnd.drupal-ajax" data-big-pipe- replacement-for-placeholder-with- id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3A renderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntEx oshbm1kh2wQzzzAA"> [{"command":"insert","method":"replaceWith","selector":"[data-big- pipe-placeholder- id=\u0022callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%

    3A%3ArenderMessages\u0026args%5B0%5D\u0026token=_HAdUpwWmet0TOTe2PSi JuMntExoshbm1kh2wQzzzAA\u0022]","data":"","settings":null}] </script>
  92. None
  93. ✓Navigation page ✓Private page Weak spots Not cached because of

    stateful content
  94. Move state client-side

  95. Replace PHP session with JSON Web Tokens

  96. JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pb iIsImV4cCI6MTQ5NTUyODc1NiwibG9naW4iOnRydWV9.u4Idy- SYnrFdnH1h9_sNc4OasORBJcrh2fPo1EOTre8 ✓3 parts ✓Dot separated ✓Base64 encoded

    JSON ✓Header ✓Payload ✓Signature (HMAC with secret)
  97. eyJzdWIiOiJhZG1pbiIsIm V4cCI6MTQ5NTUyODc1Niwi bG9naW4iOnRydWV9 { "alg": "HS256", "typ": "JWT" } {

    "sub": "admin", "exp": 1495528756, "login": true } HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) eyJhbGciOiJIUzI1NiIsI nR5cCI6IkpXVCJ9 u4Idy- SYnrFdnH1h9_sNc4OasOR BJcrh2fPo1EOTre8
  98. JWT Cookie:token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJz dWIiOiJhZG1pbiIsImV4cCI6MTQ5NTUyODc1NiwibG9naW4iOnRydW V9.u4Idy-SYnrFdnH1h9_sNc4OasORBJcrh2fPo1EOTre8 ✓Stored in a cookie ✓Can be

    validated by Varnish ✓Payload can be processed by any language (e.g. Javascript)
  99. sub jwt { std.log("Ready to perform some JWT magic"); if(cookie.isset("jwt_cookie"))

    { #Extract header data from JWT var.set("token", cookie.get("jwt_cookie")); var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1")); var.set("type", regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1")); var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*?$"},"\1")); #Don't allow invalid JWT header if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") { #Extract signature & payload data from JWT var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1")); var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1")); var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload")))); var.set("payload", digest.base64url_decode(var.get("rawPayload"))); var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1")); var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1")); var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1")); var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)".*?$"},"\1")); #Only allow valid userId if(var.get("userId") ~ "^\d+$") { #Don't allow expired JWT if(std.time(var.get("exp"),now) >= now) { #SessionId should match JTI value from JWT if(cookie.get(var.get("sessionCookie")) == var.get("jti")) { #Don't allow invalid JWT signature if(var.get("signature") == var.get("currentSignature")) { #The sweet spot set req.http.X-login="true"; } else { std.log("JWT: signature doesn't match. Received: " + var.get("signature") + ", expected: " + var.get("currentSignature")); } } else { std.log("JWT: session cookie doesn't match JTI." + var.get("sessionCookie") + ": " + cookie.get(var.get("sessionCookie")) + ", JTI:" + var.get("jti")); } } else { std.log("JWT: token has expired"); } } else { std.log("UserId '"+ var.get("userId") +"', is not numeric"); } } else { std.log("JWT: type is not JWT or algorithm is not HS256"); } std.log("JWT processing finished. UserId: " + var.get("userId") + ". X-Login: " + req.http.X-login); } #Look for full private content if(req.url ~ "/node/2" && req.url !~ "^/user/login") { if(req.http.X-login != "true") { return(synth(302,"/user/login?destination=" + req.url)); } } } Insert incomprehensible Varnish VCL code here …
  100. X-Login: true End result: X-Login: false Custom request header set

    by Varnish
  101. Extra cache variation required

  102. Vary: Accept-Language, X-Login Content for logged-in & anonymous differs

  103. <script language="JavaScript">
 function getCookie(name) {
 var value = "; "

    + document.cookie;
 var parts = value.split("; " + name + "=");
 if (parts.length == 2) return parts.pop().split(";").shift();
 }
 function parseJwt (token) {
 var base64Url = token.split('.')[1];
 var base64 = base64Url.replace('-', '+').replace('_', '/');
 return JSON.parse(window.atob(base64));
 };
 $(document).ready(function(){
 if ($.cookie('token') != null ){
 var token = parseJwt($.cookie("token"));
 $("#usernameLabel").html(', ' + token.sub);
 }
 });
 </script> Parse JWT in Javascript
  104. Does not require backend access

  105. composer require firebase/php-jwt

  106. <?php namespace App\Service; use Firebase\JWT\JWT; use Symfony\Component\HttpFoundation\Cookie; class JwtAuthentication {

    protected $key; protected $username; protected $password; public function __construct($key,$username,$password) { $this->key = $key; $this->username = $username; $this->password = $password; } public function jwt($username) { return JWT::encode([ 'sub'=>$username, 'exp'=>time() + (4 * 24 * 60 * 60), 'login'=>true, ],$this->key); } public function createCookie($username) { return new Cookie("token",$this->jwt($username), time() + (3600 * 48), '/', null, false, false); } public function validate($token) { src/Service/JwtAuthentication.php
  107. $this->password = $password; } public function jwt($username) { return JWT::encode([

    'sub'=>$username, 'exp'=>time() + (4 * 24 * 60 * 60), //'exp'=>time() + 60, 'login'=>true, ],$this->key); } public function createCookie($username) { return new Cookie("token",$this->jwt($username), time() + (3600 * 48), '/', null, false, false); } public function validate($token) { try { $data = JWT::decode($token,$this->key,['HS256']); $data = (array)$data; if($data['sub'] !== $this->username) { return false; } return true; } catch(\UnexpectedValueException $e) { return false; } } } src/Service/JwtAuthentication.php
  108. services: App\Service\JwtAuthentication: arguments: $key: '%env(JWT_KEY)%' $username: '%env(JWT_USERNAME)%' $password: '%env(JWT_PASSWORD)%' src/Service/JwtAuthentication.php

  109. ###> JWT authentication ### JWT_KEY=SlowWebSitesSuck JWT_USERNAME=admin JWT_PASSWORD=$2y$10$431rvq1qS9ewNFP0Gti/o.kBbuMK4zs8IDTLlxm5uzV7cbv8wKt0K ###< JWT authentication

    ### .env
  110. /** * @Route("/login", name="login", methods="GET") */ public function login(Request $request,

    JwtAuthentication $jwt) { if($jwt->validate($request->cookies->get('token'))) { return new RedirectResponse($this->generateUrl('home')); } $response = $this->render('login.twig',['loginLogoutUrl'=>$this- >generateUrl('login'),'loginLogoutLabel'=>'log_in']); $response ->setSharedMaxAge(500) ->setVary('X-Login',false) ->setPublic(); return $response; } /** * @Route("/login", name="loginpost", methods="POST") */ public function loginpost(Request $request, JwtAuthentication $jwt) { $username = $request->get('username'); $password = $request->get('password'); if(!$username || !$password || getenv('JWT_USERNAME') != $username || ! password_verify($password,getenv('JWT_PASSWORD'))) { return new RedirectResponse($this->generateUrl('login')); } $response = new RedirectResponse($this->generateUrl('home')); $response->headers->setCookie($jwt->createCookie($username)); return $response; } src/Controller/DefaultController.php
  111. /** * @Route("/logout", name="logout") */ public function logout() { $response

    = new RedirectResponse($this->generateUrl('login')); $response->headers->clearCookie('token'); return $response; } src/Controller/DefaultController.php
  112. /** * @Route("/nav", name="nav") */ public function nav(Request $request, JwtAuthentication

    $jwt) { if($jwt->validate($request->cookies->get('token'))) { $loginLogoutUrl = $loginLogoutUrl = $this->generateUrl('logout'); $loginLogoutLabel = 'log_out'; } else { $loginLogoutUrl = $this->generateUrl('login'); $loginLogoutLabel = 'log_in'; } $response = $this->render('nav.twig', ['loginLogoutUrl'=>$loginLogoutUrl,'loginLogoutLabel'=>$loginLogoutLabel]); $response ->setVary('X-Login',false) ->setSharedMaxAge(500) ->setPublic(); return $response; } src/Controller/DefaultController.php
  113. {% extends "base.twig" %} {% block title %}Login{% endblock %}

    {% block content %} <div class="page-header"> <h1>{{ 'log_in' | trans }}</h1> </div> <form class="form-horizontal" method="post" action="{{ url('loginpost') }}"> <div class="form-group"> <label for="usernameInput" class="col-sm-2 control-label">{{ 'username' | trans }}</label> <div class="col-sm-6"> <input type="text" name="username" class="form-control" id="usernameInput" placeholder="{{ 'username' | trans }}"> </div> </div> <div class="form-group"> <label for="passwordInput" class="col-sm-2 control-label">{{ 'password' | trans }}</label> <div class="col-sm-6"> <input type="password" name="password" class="form-control" id="passwordInput" placeholder="{{ 'password' | trans }}"> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-default">{{ 'log_in' | trans }}</button> </div> </div> </form> {% endblock %} templates/login.twig
  114. None
  115. None
  116. Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1Ni J9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTUyMDk0M jExNSwibG9naW4iOnRydWV9._161Lf8BKkbzjqVv 5d62O5aMdCotKvCqd7F8qFqZC2Y

  117. With the proper VCL, Varnish can process the JWT and

    make cache variations for authenticated & anonymous content
  118. https://github.com/ThijsFeryn/ cacheable-sites-symfony4

  119. https://feryn.eu https://twitter.com/ThijsFeryn https://instagram.com/ThijsFeryn

  120. None