# 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 强关联起来了。具体实现步骤大致如下:
首先,每个 Session 实例都需要绑定一个
uid
,如果是用户 Session 的话,那么直接用 userId 作为uid
,而如果你只是临时用 session 来存一下验证码的话, 那你多半是没法提供 userId 的,此时系统会为你自动生成一个 32 位字符串作为uid
。对于同一个
uid
的用户,在不同的客户端登录,系统会为你再生成一个随机的seed
(seed 生成的默认实现是获取当前访问的微秒时间)。这样不同客户端之间的会话数据既相互关联, 同时又能保持数据相互隔离。将
uid
用私钥加密之后就得到sessionId
,通过这个sessionId
就能把当前用户的所有会话数据全部读取出来,在用seed
来区分不同客户端的会话数据。Session 创建完成之后,服务端会给客户端生成一个访问的 token,token 的数据结构如下:
{ "uid": "123", "seed": "xxxxxx", "addr": "123.456.789.110", "sign": "xxxxxx" }
- uid: 用户 ID
- seed: 客户端的专属种子,每个客户端不一样
- addr: 当前客户端最后一次访问的 IP 地址
- sign: token 签名信息,确保 token 信息没有被客户端改变
将 token 生成
base64
存储在客户端的 cookie 中, 客户端可以凭借该 token 访问服务。目前支持传送 token 的方式有:- 直接 GET 或者 POST 传参的形式
- 通过 cookie 传送
- 通过头信息传送