diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 157454d..67ae505 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -18,6 +18,7 @@
 use Drupal\Core\PageCache\ResponsePolicyInterface;
 use Drupal\Core\Routing\AccessAwareRouterInterface;
 use Drupal\Core\Site\Settings;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -92,7 +93,7 @@ public function __construct(LanguageManagerInterface $language_manager, ConfigFa
   /**
    * Sets extra headers on successful responses.
    *
-   * @param Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
    *   The event to process.
    */
   public function onRespond(FilterResponseEvent $event) {
@@ -136,7 +137,13 @@ public function onRespond(FilterResponseEvent $event) {
     // Apply the request's access result cacheability metadata, if it has any.
     $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT);
     if ($access_result instanceof CacheableDependencyInterface) {
-      $this->updateDrupalCacheHeaders($response, $access_result);
+      $this->updateDrupalCacheHeaders($response, $access_result->getCacheTags(), $access_result->getCacheContexts());
+    }
+
+    // Apply the route's cache tags information, if it has any.
+    $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT);
+    if ($route && ($cache_tags = $route->getOption('cache_tags'))) {
+      $this->updateDrupalCacheHeaders($response, $cache_tags);
     }
 
     $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY);
@@ -162,24 +169,30 @@ public function onRespond(FilterResponseEvent $event) {
    * Updates Drupal's cache headers using the route's cacheable access result.
    *
    * @param \Symfony\Component\HttpFoundation\Response $response
-   * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheable_access_result
+   *   A Response object.
+   * @param string[] $cache_tags
+   *   A set of cache tags.
+   * @param string[] $cache_contexts
+   *   A set of cache contexts.
    */
-  protected function updateDrupalCacheHeaders(Response $response, CacheableDependencyInterface $cacheable_access_result) {
+  protected function updateDrupalCacheHeaders(Response $response, array $cache_tags = [], array $cache_contexts = []) {
     // X-Drupal-Cache-Tags
-    $cache_tags = $cacheable_access_result->getCacheTags();
-    if ($response->headers->has('X-Drupal-Cache-Tags')) {
-      $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
-      $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags);
+    if ($cache_tags) {
+      if ($response->headers->has('X-Drupal-Cache-Tags')) {
+        $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
+        $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags);
+      }
+      $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags));
     }
-    $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags));
 
     // X-Drupal-Cache-Contexts
-    $cache_contexts = $cacheable_access_result->getCacheContexts();
-    if ($response->headers->has('X-Drupal-Cache-Contexts')) {
-      $existing_cache_contexts = explode(' ', $response->headers->get('X-Drupal-Cache-Contexts'));
-      $cache_contexts = Cache::mergeContexts($existing_cache_contexts, $cache_contexts);
+    if ($cache_contexts) {
+      if ($response->headers->has('X-Drupal-Cache-Contexts')) {
+        $existing_cache_contexts = explode(' ', $response->headers->get('X-Drupal-Cache-Contexts'));
+        $cache_contexts = Cache::mergeContexts($existing_cache_contexts, $cache_contexts);
+      }
+      $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContexts->optimizeTokens($cache_contexts)));
     }
-    $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContexts->optimizeTokens($cache_contexts)));
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Bootstrap/PageCacheTest.php b/core/modules/system/src/Tests/Bootstrap/PageCacheTest.php
index 4256c16..ff417cb 100644
--- a/core/modules/system/src/Tests/Bootstrap/PageCacheTest.php
+++ b/core/modules/system/src/Tests/Bootstrap/PageCacheTest.php
@@ -68,6 +68,9 @@ function testPageCacheTags() {
       'pre_render',
       'rendered',
       'system_test_cache_tags_page',
+      // Check that cache tags added as a route option are present.
+      'system_test_route_cache_tag_1',
+      'system_test_route_cache_tag_2',
     );
     $this->assertIdentical($cache_entry->tags, $expected_tags);
 
diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php
index e0a4f34..b567f28 100644
--- a/core/modules/system/src/Tests/Routing/RouterTest.php
+++ b/core/modules/system/src/Tests/Routing/RouterTest.php
@@ -71,13 +71,13 @@ public function testFinishResponseSubscriber() {
     // 3. controller result: Response object, globally cacheable route access.
     $this->drupalGet('router_test/test1');
     $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-drupal-cache-contexts'], '');
-    $this->assertEqual($headers['x-drupal-cache-tags'], '');
+    $this->assertTrue(!isset($headers['x-drupal-cache-contexts']));
+    $this->assertTrue(!isset($headers['x-drupal-cache-tags']));
     // 4. controller result: Response object, per-role cacheable route access.
     $this->drupalGet('router_test/test20');
     $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles');
-    $this->assertEqual($headers['x-drupal-cache-tags'], '');
+    $this->assertTrue(!isset($headers['x-drupal-cache-tags']));
   }
 
   /**
diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml
index ecb9921..4258afe 100644
--- a/core/modules/system/tests/modules/system_test/system_test.routing.yml
+++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml
@@ -63,6 +63,10 @@ system_test.cache_tags_page:
   path: '/system-test/cache_tags_page'
   defaults:
     _controller: '\Drupal\system_test\Controller\SystemTestController::system_test_cache_tags_page'
+  options:
+    cache_tags:
+      - system_test_route_cache_tag_1
+      - system_test_route_cache_tag_2
   requirements:
     _access: 'TRUE'
 
diff --git a/core/modules/user/src/Plugin/views/access/Permission.php b/core/modules/user/src/Plugin/views/access/Permission.php
index 6dca9bd..2b71b0d 100644
--- a/core/modules/user/src/Plugin/views/access/Permission.php
+++ b/core/modules/user/src/Plugin/views/access/Permission.php
@@ -80,6 +80,7 @@ public function access(AccountInterface $account) {
    * {@inheritdoc}
    */
   public function alterRouteDefinition(Route $route) {
+    parent::alterRouteDefinition($route);
     $route->setRequirement('_permission', $this->options['perm']);
   }
 
diff --git a/core/modules/user/src/Plugin/views/access/Role.php b/core/modules/user/src/Plugin/views/access/Role.php
index 6a106de..857710b 100644
--- a/core/modules/user/src/Plugin/views/access/Role.php
+++ b/core/modules/user/src/Plugin/views/access/Role.php
@@ -80,6 +80,7 @@ public function access(AccountInterface $account) {
    * {@inheritdoc}
    */
   public function alterRouteDefinition(Route $route) {
+    parent::alterRouteDefinition($route);
     if ($this->options['role']) {
       $route->setRequirement('_role', (string) implode('+', $this->options['role']));
     }
diff --git a/core/modules/views/src/Plugin/views/access/AccessPluginBase.php b/core/modules/views/src/Plugin/views/access/AccessPluginBase.php
index c0aaa5d..10ce3a3 100644
--- a/core/modules/views/src/Plugin/views/access/AccessPluginBase.php
+++ b/core/modules/views/src/Plugin/views/access/AccessPluginBase.php
@@ -59,7 +59,15 @@ public function summaryTitle() {
    * @param \Symfony\Component\Routing\Route $route
    *   The route to change.
    */
-  abstract public function alterRouteDefinition(Route $route);
+  public function alterRouteDefinition(Route $route) {
+    // Views access plugins can change through configuration whether a user has
+    // access to a resource or not, so we have to add a cache tag on the route
+    // in order to allow cache tag invalidation to clear the page cache for this
+    // view (e.g. when a 403 views resource is cached for a subset of users and
+    // the access plugin configuration is changed, the cached 403 output has to
+    // be invalidated).
+    $route->setOption('cache_tags', $this->view->storage->getCacheTags());
+  }
 
 }
 
diff --git a/core/modules/views/src/Plugin/views/access/None.php b/core/modules/views/src/Plugin/views/access/None.php
index fa1789b..17e6777 100644
--- a/core/modules/views/src/Plugin/views/access/None.php
+++ b/core/modules/views/src/Plugin/views/access/None.php
@@ -42,6 +42,7 @@ public function access(AccountInterface $account) {
    * {@inheritdoc}
    */
   public function alterRouteDefinition(Route $route) {
+    parent::alterRouteDefinition($route);
     $route->setRequirement('_access', 'TRUE');
   }
 
diff --git a/core/modules/views/src/Tests/Plugin/AccessTest.php b/core/modules/views/src/Tests/Plugin/AccessTest.php
index 95ef502..ea11ba6 100644
--- a/core/modules/views/src/Tests/Plugin/AccessTest.php
+++ b/core/modules/views/src/Tests/Plugin/AccessTest.php
@@ -102,18 +102,11 @@ function testStaticAccessPlugin() {
     // termination event fires. Simulate that here.
     $this->container->get('router.builder')->rebuildIfNeeded();
 
-    // Clear the page cache.
-    // @todo Remove as part of https://www.drupal.org/node/2464657. The root
-    //   cause is that the access plugins alters the route's access
-    //   requirements. That means that the 403 from above does not have any
-    //   cache tags, so modifying the View entity does not cause the cached 403
-    //   page to be invalidated.
-    Cache::invalidateTags(['rendered']);
-
     $this->assertTrue($access_plugin->access($this->normalUser));
 
     $this->drupalGet('test_access_static');
     $this->assertResponse(200);
+    $this->assertCacheTag($view->storage->getCacheTags()[0]);
   }
 
 }
diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/access/StaticTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/access/StaticTest.php
index c2be665..4c73f7d 100644
--- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/access/StaticTest.php
+++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/access/StaticTest.php
@@ -40,6 +40,7 @@ public function access(AccountInterface $account) {
    * {@inheritdoc}
    */
   public function alterRouteDefinition(Route $route) {
+    parent::alterRouteDefinition($route);
     if (!empty($this->options['access'])) {
       $route->setRequirement('_access', 'TRUE');
     }
