diff --git a/core/core.services.yml b/core/core.services.yml
index 51ce4c4..44711b0 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -718,6 +718,11 @@ services:
     arguments: ['@authentication']
     calls:
       - [setRequest, ['@?request=']]
+  session:
+    class: Drupal\Core\Session\Storage
+    arguments: ['@database']
+    tags:
+      - { name: persist }
   asset.css.collection_renderer:
     class: Drupal\Core\Asset\CssCollectionRenderer
     arguments: [ '@state' ]
diff --git a/core/includes/session.inc b/core/includes/session.inc
index 76aa83b..17559e6 100644
--- a/core/includes/session.inc
+++ b/core/includes/session.inc
@@ -5,52 +5,11 @@
  * User session handling functions.
  */
 
-use Drupal\Component\Utility\Crypt;
-use Drupal\Component\Utility\Settings;
-use Drupal\Core\Session\AnonymousUserSession;
-use Drupal\Core\Session\SessionHandler;
-
 /**
  * Initializes the session handler, starting a session if needed.
  */
 function drupal_session_initialize() {
-  global $user;
-
-  // Register the default session handler.
-  // @todo: Extract session storage from session handler into a service.
-  $handler = new SessionHandler(\Drupal::request(), \Drupal::database());
-  session_set_save_handler($handler, TRUE);
-
-  $is_https = \Drupal::request()->isSecure();
-  $cookies = \Drupal::request()->cookies;
-  if (($cookies->has(session_name()) && ($session_name = $cookies->get(session_name()))) || ($is_https && Settings::get('mixed_mode_sessions', FALSE) && ($cookies->has(substr(session_name(), 1))) && ($session_name = $cookies->get(substr(session_name(), 1))))) {
-    // If a session cookie exists, initialize the session. Otherwise the
-    // session is only started on demand in drupal_session_commit(), making
-    // anonymous users not use a session cookie unless something is stored in
-    // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
-    drupal_session_start();
-    if ($user->isAuthenticated() || !empty($_SESSION)) {
-      drupal_page_is_cacheable(FALSE);
-    }
-  }
-  else {
-    // Set a session identifier for this request. This is necessary because
-    // we lazily start sessions at the end of this request, and some
-    // processes (like drupal_get_token()) needs to know the future
-    // session ID in advance.
-    $GLOBALS['lazy_session'] = TRUE;
-    $user = new AnonymousUserSession();
-    // Less random sessions (which are much faster to generate) are used for
-    // anonymous users than are generated in drupal_session_regenerate() when
-    // a user becomes authenticated.
-    session_id(Crypt::randomBytesBase64());
-    if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
-      $insecure_session_name = substr(session_name(), 1);
-      $session_id = Crypt::randomBytesBase64();
-      $cookies->set($insecure_session_name, $session_id);
-    }
-  }
-  date_default_timezone_set(drupal_get_user_timezone());
+  \Drupal::service('session')->initialize();
 }
 
 /**
@@ -59,19 +18,7 @@ function drupal_session_initialize() {
  * @ingroup php_wrappers
  */
 function drupal_session_start() {
-  // Command line clients do not support cookies nor sessions.
-  if (!drupal_session_started() && !drupal_is_cli()) {
-    // Save current session data before starting it, as PHP will destroy it.
-    $session_data = isset($_SESSION) ? $_SESSION : NULL;
-
-    session_start();
-    drupal_session_started(TRUE);
-
-    // Restore session data.
-    if (!empty($session_data)) {
-      $_SESSION += $session_data;
-    }
-  }
+  \Drupal::service('session')->start();
 }
 
 /**
@@ -80,47 +27,14 @@ function drupal_session_start() {
  * If an anonymous user already have an empty session, destroy it.
  */
 function drupal_session_commit() {
-  global $user;
-
-  if (!drupal_save_session()) {
-    // We don't have anything to do if we are not allowed to save the session.
-    return;
-  }
-
-  if ($user->isAnonymous() && empty($_SESSION)) {
-    // There is no session data to store, destroy the session if it was
-    // previously started.
-    if (drupal_session_started()) {
-      session_destroy();
-    }
-  }
-  else {
-    // There is session data to store. Start the session if it is not already
-    // started.
-    if (!drupal_session_started()) {
-      drupal_session_start();
-      if (\Drupal::request()->isSecure() && Settings::get('mixed_mode_sessions', FALSE)) {
-        $insecure_session_name = substr(session_name(), 1);
-        $params = session_get_cookie_params();
-        $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
-        $cookie_params = \Drupal::request()->cookies;
-        setcookie($insecure_session_name, $cookie_params->get($insecure_session_name), $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
-      }
-    }
-    // Write the session data.
-    session_write_close();
-  }
+  \Drupal::service('session')->save();
 }
 
 /**
  * Returns whether a session has been started.
  */
-function drupal_session_started($set = NULL) {
-  static $session_started = FALSE;
-  if (isset($set)) {
-    $session_started = $set;
-  }
-  return $session_started && session_status() === \PHP_SESSION_ACTIVE;
+function drupal_session_started() {
+  return \Drupal::service('session')->isStarted();
 }
 
 /**
@@ -129,72 +43,7 @@ function drupal_session_started($set = NULL) {
  * @ingroup php_wrappers
  */
 function drupal_session_regenerate() {
-  global $user;
-
-  // Nothing to do if we are not allowed to change the session.
-  if (!drupal_save_session()) {
-    return;
-  }
-
-  $is_https = \Drupal::request()->isSecure();
-  $cookies = \Drupal::request()->cookies;
-
-  if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
-    $insecure_session_name = substr(session_name(), 1);
-    if (!isset($GLOBALS['lazy_session']) && $cookies->has($insecure_session_name)) {
-      $old_insecure_session_id = $cookies->get($insecure_session_name);
-    }
-    $params = session_get_cookie_params();
-    $session_id = Crypt::randomBytesBase64();
-    // If a session cookie lifetime is set, the session will expire
-    // $params['lifetime'] seconds from the current request. If it is not set,
-    // it will expire when the browser is closed.
-    $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
-    setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
-    $cookies->set($insecure_session_name, $session_id);
-  }
-
-  if (drupal_session_started()) {
-    $old_session_id = session_id();
-  }
-  session_id(Crypt::randomBytesBase64());
-
-  if (isset($old_session_id)) {
-    $params = session_get_cookie_params();
-    $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
-    setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
-    $fields = array('sid' => Crypt::hashBase64(session_id()));
-    if ($is_https) {
-      $fields['ssid'] = Crypt::hashBase64(session_id());
-      // If the "secure pages" setting is enabled, use the newly-created
-      // insecure session identifier as the regenerated sid.
-      if (Settings::get('mixed_mode_sessions', FALSE)) {
-        $fields['sid'] = Crypt::hashBase64($session_id);
-      }
-    }
-    db_update('sessions')
-      ->fields($fields)
-      ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($old_session_id))
-      ->execute();
-  }
-  elseif (isset($old_insecure_session_id)) {
-    // If logging in to the secure site, and there was no active session on the
-    // secure site but a session was active on the insecure site, update the
-    // insecure session with the new session identifiers.
-    db_update('sessions')
-      ->fields(array('sid' => Crypt::hashBase64($session_id), 'ssid' => Crypt::hashBase64(session_id())))
-      ->condition('sid', Crypt::hashBase64($old_insecure_session_id))
-      ->execute();
-  }
-  else {
-    // Start the session when it doesn't exist yet.
-    // Preserve the logged in user, as it will be reset to anonymous
-    // by _drupal_session_read.
-    $account = $user;
-    drupal_session_start();
-    $user = $account;
-  }
-  date_default_timezone_set(drupal_get_user_timezone());
+  \Drupal::service('session')->regenerate();
 }
 
 /**
@@ -205,10 +54,9 @@ function drupal_session_regenerate() {
  */
 function drupal_session_destroy_uid($uid) {
   // Nothing to do if we are not allowed to change the session.
-  if (!drupal_save_session()) {
+  if (!\Drupal::service('session')->isEnabled()) {
     return;
   }
-
   db_delete('sessions')
     ->condition('uid', $uid)
     ->execute();
@@ -230,12 +78,15 @@ function drupal_session_destroy_uid($uid) {
  *   FALSE if writing session data has been disabled. Otherwise, TRUE.
  */
 function drupal_save_session($status = NULL) {
-  // PHP session ID, session, and cookie handling happens in the global scope.
-  // This value has to persist across calls to drupal_static_reset(), since a
-  // potentially wrong or disallowed session would be written otherwise.
-  static $save_session = TRUE;
-  if (isset($status)) {
-    $save_session = $status;
+  if (!isset($status)) {
+    return \Drupal::service('session')->isEnabled();
+  }
+  if ($status) {
+    \Drupal::service('session')->enabled();
+    return TRUE;
+  }
+  else {
+    \Drupal::service('session')->disable();
+    return FALSE;
   }
-  return $save_session;
 }
diff --git a/core/lib/Drupal/Core/Session/Storage.php b/core/lib/Drupal/Core/Session/Storage.php
new file mode 100644
index 0000000..4f1afb7
--- /dev/null
+++ b/core/lib/Drupal/Core/Session/Storage.php
@@ -0,0 +1,282 @@
+<?php
+
+/**
+ * @file
+ * User session handling functions.
+ */
+
+namespace Drupal\Core\Session;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Session\AnonymousUserSession;
+use Drupal\Core\Session\SessionHandler;
+
+class Storage {
+
+  /**
+   * The database connection to use.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Whether a lazy session has been started.
+   *
+   * @var bool
+   */
+  protected $lazySession;
+
+  /**
+   * Whether session management is enabled or temporarily disabled.
+   *
+   * PHP session ID, session, and cookie handling happens in the global scope.
+   * This value has to persist, since a potentially wrong or disallowed session
+   * would be written otherwise.
+   *
+   * @var bool
+   */
+  protected static $enabled = TRUE;
+
+  /**
+   * Whether the session has been started.
+   *
+   * @var bool
+   */
+  protected $started = FALSE;
+
+  /**
+   * Construct the session storage.
+   */
+  public function __construct(Connection $connection) {
+    $this->connection = $connection;
+  }
+
+  /**
+   * Initializes the session handler, starting a session if needed.
+   */
+  public function initialize() {
+    global $user;
+
+    // Register the default session handler.
+    // @todo Extract session storage from session handler into a service.
+    $handler = new SessionHandler(\Drupal::request(), $this->connection);
+    session_set_save_handler($handler, TRUE);
+
+    $is_https = \Drupal::request()->isSecure();
+    $cookies = \Drupal::request()->cookies;
+    if (($cookies->has(session_name()) && ($session_name = $cookies->get(session_name()))) || ($is_https && Settings::get('mixed_mode_sessions', FALSE) && ($cookies->has(substr(session_name(), 1))) && ($session_name = $cookies->get(substr(session_name(), 1))))) {
+      // If a session cookie exists, initialize the session. Otherwise the
+      // session is only started on demand in drupal_session_commit(), making
+      // anonymous users not use a session cookie unless something is stored in
+      // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
+      $this->start();
+      if ($user->isAuthenticated() || !empty($_SESSION)) {
+        drupal_page_is_cacheable(FALSE);
+      }
+    }
+    else {
+      // Set a session identifier for this request. This is necessary because
+      // we lazily start sessions at the end of this request, and some
+      // processes (like drupal_get_token()) needs to know the future
+      // session ID in advance.
+      $this->lazySession = TRUE;
+      $user = new AnonymousUserSession();
+      // Less random sessions (which are much faster to generate) are used for
+      // anonymous users than are generated in drupal_session_regenerate() when
+      // a user becomes authenticated.
+      session_id(Crypt::randomBytesBase64());
+      if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
+        $insecure_session_name = substr(session_name(), 1);
+        $session_id = Crypt::randomBytesBase64();
+        $cookies->set($insecure_session_name, $session_id);
+      }
+    }
+    date_default_timezone_set(drupal_get_user_timezone());
+  }
+
+  /**
+   * Starts a session forcefully, preserving already set session data.
+   */
+  public function start() {
+    if ($this->isCli() || $this->isStarted()) {
+      return;
+    }
+    // Save current session data before starting it, as PHP will destroy it.
+    $session_data = isset($_SESSION) ? $_SESSION : NULL;
+
+    session_start();
+    $this->started = TRUE;
+
+    // Restore session data.
+    if (!empty($session_data)) {
+      $_SESSION += $session_data;
+    }
+  }
+
+  /**
+   * Commits the current session, if necessary.
+   *
+   * If an anonymous user already have an empty session, destroy it.
+   */
+  public function save() {
+    global $user;
+
+    if (!$this->isEnabled()) {
+      // We don't have anything to do if we are not allowed to save the session.
+      return;
+    }
+
+    if ($user->isAnonymous() && empty($_SESSION)) {
+      // There is no session data to store, destroy the session if it was
+      // previously started.
+      if ($this->isStarted()) {
+        session_destroy();
+      }
+    }
+    else {
+      // There is session data to store. Start the session if it is not already
+      // started.
+      if (!$this->isStarted()) {
+        $this->start();
+        if (\Drupal::request()->isSecure() && Settings::get('mixed_mode_sessions', FALSE)) {
+          $insecure_session_name = substr(session_name(), 1);
+          $params = session_get_cookie_params();
+          $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+          $cookie_params = \Drupal::request()->cookies;
+          setcookie($insecure_session_name, $cookie_params->get($insecure_session_name), $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
+        }
+      }
+      // Write the session data.
+      session_write_close();
+    }
+  }
+
+  /**
+   * Returns whether a session has been started.
+   */
+  public function isStarted() {
+    return $this->started && session_status() === \PHP_SESSION_ACTIVE;
+  }
+
+  /**
+   * Called when an anonymous user becomes authenticated or vice-versa.
+   */
+  public function regenerate() {
+    global $user;
+
+    // Nothing to do if we are not allowed to change the session.
+    if (!$this->isEnabled()) {
+      return;
+    }
+
+    $is_https = \Drupal::request()->isSecure();
+    $cookies = \Drupal::request()->cookies;
+
+    if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
+      $insecure_session_name = substr(session_name(), 1);
+      if (!isset($this->lazySession) && $cookies->has($insecure_session_name)) {
+        $old_insecure_session_id = $cookies->get($insecure_session_name);
+      }
+      $params = session_get_cookie_params();
+      $session_id = Crypt::randomBytesBase64();
+      // If a session cookie lifetime is set, the session will expire
+      // $params['lifetime'] seconds from the current request. If it is not set,
+      // it will expire when the browser is closed.
+      $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+      setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
+      $cookies->set($insecure_session_name, $session_id);
+    }
+
+    if ($this->isStarted()) {
+      $old_session_id = session_id();
+    }
+    session_id(Crypt::randomBytesBase64());
+
+    if (isset($old_session_id)) {
+      $params = session_get_cookie_params();
+      $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
+      setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
+      $fields = array('sid' => Crypt::hashBase64(session_id()));
+      if ($is_https) {
+        $fields['ssid'] = Crypt::hashBase64(session_id());
+        // If the "secure pages" setting is enabled, use the newly-created
+        // insecure session identifier as the regenerated sid.
+        if (Settings::get('mixed_mode_sessions', FALSE)) {
+          $fields['sid'] = Crypt::hashBase64($session_id);
+        }
+      }
+      $this->connection->update('sessions')
+        ->fields($fields)
+        ->condition($is_https ? 'ssid' : 'sid', Crypt::hashBase64($old_session_id))
+        ->execute();
+    }
+    elseif (isset($old_insecure_session_id)) {
+      // If logging in to the secure site, and there was no active session on
+      // the secure site but a session was active on the insecure site, update
+      // the insecure session with the new session identifiers.
+      $this->connection->update('sessions')
+        ->fields(array('sid' => Crypt::hashBase64($session_id), 'ssid' => Crypt::hashBase64(session_id())))
+        ->condition('sid', Crypt::hashBase64($old_insecure_session_id))
+        ->execute();
+    }
+    else {
+      // Start the session when it doesn't exist yet.
+      // Preserve the logged in user, as it will be reset to anonymous
+      // by _drupal_session_read.
+      $account = $user;
+      $this->start();
+      $user = $account;
+    }
+    date_default_timezone_set(drupal_get_user_timezone());
+  }
+
+  /**
+   * Determines whether to save session data of the current request.
+   *
+   * @return bool
+   *   FALSE if writing session data has been disabled. TRUE otherwise.
+   */
+  public function isEnabled() {
+    return static::$enabled;
+  }
+
+  /**
+   * Temporarily disables saving of session data.
+   *
+   * This function allows the caller to temporarily disable writing of
+   * session data, should the request end while performing potentially
+   * dangerous operations, such as manipulating the global $user object.
+   * See http://drupal.org/node/218104 for usage.
+   *
+   * @return $this
+   */
+  public function disable() {
+    static::$enabled = FALSE;
+    return $this;
+  }
+
+  /**
+   * Re-enables saving of session data.
+   *
+   * @return $this
+   */
+  public function enable() {
+    static::$enabled = TRUE;
+    return $this;
+  }
+
+  /**
+   * Returns whether the current PHP process runs on CLI.
+   *
+   * Command line clients do not support cookies nor sessions.
+   *
+   * @return bool
+   */
+  protected function isCli() {
+    return PHP_SAPI === 'cli';
+  }
+
+}
