PAYATHON 2026

session_regenerate_id 返回两个 PHPSESSID Cookie

AI 小顾

结论

这是 PHP 的正常行为,并不表示同时存在两个有效会话。

当请求没有携带 PHPSESSID 时:

  1. session_start() 创建第一个 session ID,并添加一个 Set-Cookie 响应头。
  2. session_regenerate_id(true) 随即生成第二个 session ID,并再次添加 Set-Cookie。
  3. 如果两个 Cookie 的名称、Domain 和 Path 相同,后一个值会覆盖前一个值。

因此,客户端最终应使用最后收到的 PHPSESSID。浏览器会自动完成覆盖。如果 OkHttp 客户端没有正常登录,问题通常出在 CookieJar 未正确保存或替换 Cookie,而不是 PHP 创建了两个并行会话。

下面的代码在首次访问时确实会产生两个 Set-Cookie:

session_start();
session_regenerate_id(true);

假设请求中没有 PHPSESSID,执行过程如下:

session_start()
    -> 创建 ID A
    -> Set-Cookie: PHPSESSID=A

session_regenerate_id(true)
    -> 创建 ID B
    -> 删除 ID A 对应的旧 session 数据
    -> Set-Cookie: PHPSESSID=B

参数 true 表示删除旧 session 数据,但不会撤回已经加入响应头的第一个 Set-Cookie。

这也不是 “sticky session” 攻击,正确名称是 session fixation(会话固定攻击)。当用户从匿名状态变为已登录状态时,重新生成 session ID 是正确的防护措施。

推荐处理方式

首次创建会话时,session_start() 生成的 ID 本身就是新的随机 ID,没有必要立即再生成一次。只有请求已经携带 session Cookie,也就是存在登录前会话时,才需要在认证成功后更换 ID。

<?php

$sessionName = session_name();
$hadSessionCookie = isset($_COOKIE[$sessionName]);

session_start([
    'use_strict_mode' => 1,
    'cookie_httponly' => 1,
    'cookie_secure' => true,   // 仅在 HTTPS 下启用
    'cookie_samesite' => 'Lax',
]);

// 用户名和密码验证成功后执行
if ($hadSessionCookie) {
    if (!session_regenerate_id(true)) {
        throw new RuntimeException('Unable to regenerate session ID');
    }
}

$_SESSION['UserID'] = $userID;
$_SESSION['AuthToken'] = $authToken;

这样处理后,通常会出现以下两种情况:

  • 首次访问时,session_start() 只发送一个新 Cookie。
  • 已有会话时,session_start() 先恢复旧会话,认证成功后再由 session_regenerate_id() 发送一个新 Cookie。

session_regenerate_id() 必须在 session_start() 之后调用,因为它只能操作活动会话。颠倒两者的调用顺序并不能解决问题。

如果登录接口不需要保留匿名会话

如果 API 登录接口完全不依赖登录前的 session 数据,可以在启动会话前,直接指定一个由 PHP 生成的新 ID。这样整个响应只需发送一次 session Cookie:

<?php

session_id(session_create_id());

session_start([
    'use_strict_mode' => 1,
    'cookie_httponly' => 1,
    'cookie_secure' => true,   // 仅限 HTTPS
    'cookie_samesite' => 'Lax',
]);

$_SESSION['UserID'] = $userID;
$_SESSION['AuthToken'] = $authToken;

这种方式会放弃请求中已有的 session,因此不适合需要保留登录前购物车、CSRF token、OAuth 状态或其他匿名会话数据的流程。

不要使用 uniqid()、时间戳或普通随机字符串自行构造 session ID,应使用 PHP 提供的 session_create_id() 或 session_regenerate_id()。

即使服务器返回两个同名的 Set-Cookie,客户端也应按照接收顺序处理,让后一个值覆盖前一个值。OkHttp 并非在所有使用方式下都会自动持久化 Cookie,需要配置 CookieJar。

例如,可以使用 Java 的 Cookie 管理器:

CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);

OkHttpClient client = new OkHttpClient.Builder()
        .cookieJar(new JavaNetCookieJar(cookieManager))
        .build();

如果使用自定义 CookieJar,不能只是把 Cookie 追加到列表中,而不替换已有值。保存 Cookie 时,至少应根据以下组合判断是否为同一个 Cookie:

Name + Domain + Path

如果两个 Cookie 的名称都是 PHPSESSID,域名相同且 Path=/,后收到的值应覆盖先收到的值。

还需要注意

  • session_start() 和 session_regenerate_id() 必须在输出任何响应正文之前调用,否则可能出现 headers already sent。
  • 应在登录成功后重新生成 ID。认证失败时,不要把匿名会话变成已认证会话。
  • session.use_strict_mode=1 可以拒绝客户端提交但服务器中不存在的 session ID,建议启用。
  • 生产环境应使用 HTTPS,并为 session Cookie 设置 Secure、HttpOnly 和合适的 SameSite。
  • session_regenerate_id(true) 会立即删除旧 session 数据。在并发请求较多或网络不稳定的应用中,其他仍携带旧 ID 的请求可能因此丢失会话。是否立即删除旧数据,需要结合应用的并发模型评估。这与响应中出现两个 Set-Cookie 是两个不同的问题。
  • 不建议使用 header_remove('Set-Cookie') 直接删除第一个响应头,因为这可能同时删除 ClientID、UserID 等其他 Cookie。

最直接的处理方式是:首次请求只调用 session_start();如果已有登录前会话,则在认证成功后调用 session_regenerate_id();同时确保 OkHttp 的 Cookie 存储能够按照相同的名称、域名和路径覆盖旧值。

备注:内容仅供参考。