# Session 管理

基于安全性和扩展性的考虑,Herosphp 内置实现一套自己会话机制,文档后面会阐述设计原理,我们先来看一个简单的例子:

# 简单例子

namespace app\controller;

use herosphp\annotation\Controller;
use herosphp\annotation\Get;
use herosphp\core\BaseController;
use herosphp\core\HttpRequest;

#[Controller(IndexController::class)]
class UserController extends BaseController
{

    #[Get(uri: '/session')]
    public function session(HttpRequest $request)
    {
        $session = $request->session();
        $session->set('name', 'xiaoming');

        return $this->json(['name' => $session->get('name')]);
    }
}

通过 $request->session(); 获得 herosphp\session\Session 实例,通过实例的方法来增加、修改、删除 session 数据。

Herosphp 为每个 HttpRequest 都分配了两个 session 实例, 一个用来存储常规会话的数据,比如验证码之类的;另一个专门用于存储用户会话数据的实例:

class HttpRequest extends Request {
    ...
    // 常规会话 session 实例
    protected ?Session $_session = null;

    // 用户会话 session 实例
    protected ?Session $_usession = null;
    ...
}

这个特点是由 herosphp session 的设计算法决定,后面会有详细的说明。如果你要获取用户的会话的 Session 实例的话,需要通过 userId 来获取:

#[Get(uri: '/session')]
public function session(HttpRequest $request)
{
    $userId = $request->get('userId');
    $session = $request->session($userId);
    $session->set('username', 'herosphp');

    return $this->json(['username' => $session->get('username')]);
}

# 读取 Session

$session = $request->session();
// 1. 获取当前会话中所有数据
$session->get();
// 2. 获取 session 中某个值
$session->get($name, $default)

# 写入 Session

$session = $request->session();
$session->set($name, $value);

# 删除 Session

$session = $request->session();
$session->delete($name);

# 删除并返回当前 session 值
$session->take($name);

# 清空 Session

$session = $request->session($userId);
// 将当前用户的所有客户端都踢下线
$session->clear();
// 将所有的用户的会话数据全部销毁,即所有用户全部下线
$session->destroy();

# Session 配置

use herosphp\session\RedisSessionHandler;
use Workerman\Protocols\Http\Session\FileSessionHandler;

return [
  'lifetime' => 1800,
  // 同一个用户最多少个客户端同时在线,如果超过数量,最先登录客户端将被挤下线
  'max_clients' => 2,
  // Session 访问域名,默认当前域名有效
  'domain' => '',
  // 是否强制使用安全传输
  'secure' => false,
  // 进制 js 脚本获取 cookie 数据
  'http_only' => true,
  // 如果开启严格 Session 模式,则当用户的访问设备或者 IP 发生改变时,登录状态会失效
  'strict_mode' => false,
  // Session ID 的签名私钥
  'private_key' => 'R8ZvYP1kIR5X',

  // 使用本地文件作为 session 数据存储介质
  'handler_class' => FileSessionHandler::class,
  'handler_config' => ['save_path' => RUNTIME_PATH . 'session'],

  // 使用 Redis 作为 session 数据存储介质
  'handler_class' => RedisSessionHandler::class,
  'handler_config' => [
    'host' => '127.0.0.1', // Required
    'port' => 6379,        // Required
    'timeout' => 2,           // Optional
    'auth' => '',          // Optional
    'database' => 1,           // Optional
    'prefix' => '_session_'  // Optional
  ],

];

# 设计思想

Herosphp Session 设计的核心思想是在 SessionID 上做了一些改进。PHP 默认 Session 的实现的 SessionID 是一个随机字符串,它毫无意义,只是一个会话唯一标志而已。而我们 SessionID 和用户 ID 强关联起来了。具体实现步骤大致如下:

  1. 首先,每个 Session 实例都需要绑定一个 uid,如果是用户 Session 的话,那么直接用 userId 作为 uid,而如果你只是临时用 session 来存一下验证码的话, 那你多半是没法提供 userId 的,此时系统会为你自动生成一个 32 位字符串作为 uid

  2. 对于同一个 uid 的用户,在不同的客户端登录,系统会为你再生成一个随机的 seed(seed 生成的默认实现是获取当前访问的微秒时间)。这样不同客户端之间的会话数据既相互关联, 同时又能保持数据相互隔离。

  3. uid 用私钥加密之后就得到 sessionId,通过这个 sessionId 就能把当前用户的所有会话数据全部读取出来,在用 seed 来区分不同客户端的会话数据。

  4. Session 创建完成之后,服务端会给客户端生成一个访问的 token,token 的数据结构如下:

    {
      "uid": "123",
      "seed": "xxxxxx",
      "addr": "123.456.789.110",
      "sign": "xxxxxx"
    }
    
    • uid: 用户 ID
    • seed: 客户端的专属种子,每个客户端不一样
    • addr: 当前客户端最后一次访问的 IP 地址
    • sign: token 签名信息,确保 token 信息没有被客户端改变
  5. 将 token 生成 base64 存储在客户端的 cookie 中, 客户端可以凭借该 token 访问服务。目前支持传送 token 的方式有:

    • 直接 GET 或者 POST 传参的形式
    • 通过 cookie 传送
    • 通过头信息传送
上次更新: 10/27/2022, 11:18:25 AM