Skip to content

User Sessions in Winter Boot

When someone logs in to your app, you want the app to remember them on every page after that. That memory is called a session: the server stores a few facts (for example, the username), and the browser holds a small cookie with a session id that points to those facts.

Each server worker handles many requests over its lifetime, so session data must live in per-request objects — never in static or shared variables. Winter Boot gives you three pieces for this: SessionManager opens and saves sessions, RequestSession holds one request’s session data, and SessionOptions describes the cookie.

Ask the manager for the session, store what you need, and save it before returning. Everything is ready to autowire — no setup code is needed:

AuthController.php
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\stereotype\RestController;
use dev\winterframework\stereotype\web\PostMapping;
use dev\winterframework\web\http\HttpRequest;
use dev\winterframework\web\http\ResponseEntity;
use dev\winterframework\web\session\SessionManager;
use dev\winterframework\web\session\SessionOptions;
#[RestController]
class AuthController
{
#[Autowired]
protected SessionManager $sessions;
#[Autowired]
protected \SessionHandlerInterface $store;
// Cookie settings for this login flow (see "Cookie settings" below).
private SessionOptions $options;
public function __construct()
{
$this->options = new SessionOptions(name: 'SID', expirySecs: 3600);
}
#[PostMapping(path: '/login')]
public function login(HttpRequest $req): ResponseEntity
{
$session = $this->sessions->open($req, $this->store, $this->options);
// New id at login, so an id someone knew before login becomes useless.
$session->regenerateId();
$session->setUsername('alice');
$session->set('role', 'admin');
$res = ResponseEntity::ok(['status' => 'logged-in']);
$this->sessions->commit($session, $res, $this->store, $this->options);
return $res;
}
}

commit() saves the data and puts the session id into a cookie on the response. Always call it before returning — anything you set() without commit() is thrown away when the request ends.

Call regenerateId() whenever someone logs in or gains more rights. It keeps the data, gives the session a new id, and commit() deletes the row stored under the old id.

Open the session the same way. If the browser sent back a valid session cookie, you get the stored data; otherwise you get a fresh, empty session with a new id. An id the server does not know (made up, expired, or deleted) is never reused, so nobody can plant a session id in a victim’s browser and wait for them to log in:

$session = $this->sessions->open($req, $this->store, $this->options);
if ($session->isNew()) {
// not logged in
}
$role = $session->get('role');
$session = $this->sessions->open($req, $this->store, $this->options);
$session->destroy();
$this->sessions->commit($session, $res, $this->store, $this->options);

This deletes the stored data and tells the browser to drop the cookie. ($res is the ResponseEntity you are about to return.)

SessionOptions is a plain object — you create one per login flow with new, right where you use it. Different flows need different cookies: an admin login might last 15 minutes on HTTPS only, while a regular user login lasts all day. Give each flow its own cookie name and they stay independent, even in the same browser:

$adminSessions = new SessionOptions(
name: 'ADMINSESSID', // cookie name
expirySecs: 900, // login lifetime in seconds; 0 = until the browser closes
path: '/', // which site paths receive the cookie
domain: '', // empty = current domain only
secure: true, // send the cookie over HTTPS only
httponly: true, // hide the cookie from JavaScript
samesite: 'Strict', // 'Lax' (default), 'Strict', 'None' (needs secure) or '' to omit
);
$userSessions = new SessionOptions(
name: 'USERSESSID',
expirySecs: 86400,
path: '/',
domain: '',
secure: true,
httponly: true,
);

secure defaults to false so plain-HTTP development works; set it to true in production. samesite defaults to Lax.

Pass the right one to open() and commit(). Both flows can share the same store — the cookie name keeps their sessions apart. If you only ever need one kind of login, one SessionOptions is enough.

Two fields travel with every session: the username and the session type. Set them at login:

$session->setUsername('alice');
$session->setSessionType(1);

Both are loaded back automatically on every later request. A save that does not set a username keeps the stored one, so it cannot be wiped by accident — only a new name replaces it. The session type is a number your app defines (for example, 1 for admins); 0 means “no type set”.

Session data is saved with serialize(). If an attacker can write to your session store (database, Redis), they could plant objects that run code when the session is read back. List the classes your sessions may contain and everything else is refused:

application.yml
winter:
security:
unserialize:
allowedClasses: [App\Dto\Cart] # [] = scalars and arrays only

The same list applies to the shared KV cache and shared queue. Without the key, any class is allowed.

Out of the box, sessions are stored in files on the server — fine for getting started or a single server. For several servers, or sessions that must survive a restart, use a database or Redis instead by declaring one store bean (it must return \SessionHandlerInterface):

Create the table once:

CREATE TABLE winter_sessions (
session_id VARCHAR(128) NOT NULL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
expiry BIGINT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
session_type SMALLINT NOT NULL DEFAULT 0,
session_data TEXT
);

Then expose the store:

SessionConfig.php
use dev\winterframework\pdbc\PdbcTemplate;
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\stereotype\Bean;
use dev\winterframework\stereotype\Configuration;
use dev\winterframework\web\session\PdbcSessionStore;
#[Configuration]
class SessionConfig
{
#[Autowired]
protected PdbcTemplate $pdbc;
#[Bean]
public function sessionStore(): \SessionHandlerInterface
{
return new PdbcSessionStore($this->pdbc, table: 'winter_sessions', ttlSecs: 3600);
}
}

created_at records when the session was first saved; updated_at refreshes on every save. Expired rows are ignored when sessions are read, but they are not deleted for you — call the store’s gc() method from a scheduled task to purge them:

#[Scheduled(fixedRate: 3600)] // every hour (values are in seconds)
public function purgeSessions(): void {
$this->sessionStore->gc(0);
}

The winter-data-redis module offers dev\winterframework\data\redis\session\RedisSessionStore, which keeps each session as a hash under keyPrefix + sessionId and lets Redis expire old sessions by itself. Like the database store, it persists username and session type with the same write-once rule:

SessionConfig.php
use dev\winterframework\data\redis\phpredis\PhpRedisTemplate;
use dev\winterframework\data\redis\session\RedisSessionStore;
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\stereotype\Bean;
use dev\winterframework\stereotype\Configuration;
#[Configuration]
class SessionConfig
{
#[Autowired]
protected PhpRedisTemplate $redis;
#[Bean]
public function sessionStore(): \SessionHandlerInterface
{
return new RedisSessionStore($this->redis, keyPrefix: 'myapp:sess:', ttlSecs: 3600);
}
}
What you call What it does
getId() The session id from the browser’s cookie, or a fresh one
isNew() True when there was no saved session for the browser’s id (not logged in); the session then has a new id
get($key, $default = null) Read one stored value
set($key, $value) Store one value (kept only after commit())
remove($key) Delete one stored value
destroy() Log out: delete everything on the next commit(), including rows under ids replaced by regenerateId() in the same request
regenerateId() Give the session a new id (call at login); the old row is deleted on commit()
getUsername() / setUsername($u) Who the session belongs to
getSessionType() / setSessionType($t) Your own session category number