hyperf中$this->user_id会被其他请求读到,因为userservice默认是单例,整个worker进程只初始化一次,该属性在协程间共享且不自动隔离,a请求写入后b请求直接读取,造成数据串流。

Hyperf 协程里 $this->user_id 为什么会被别的请求读到
因为 UserService 默认是单例,整个 Worker 进程只初始化一次,$this->user_id 是实例属性,协程切换时不会自动清空或隔离——A 请求写入 $this->user_id = 1001,B 请求紧接着读,拿到的就是 1001,哪怕它本该是 2002。
这不是 bug,是 PHP + Swoole 协程模型的天然行为:协程共享对象实例,但不共享上下文变量。常见高危写法包括:
-
$this->cache = []后在方法里反复写入,缓存混用 -
$this->errors[] = 'xxx'不重置,数组越滚越大 -
static $counter++,所有协程共用一个计数器 - 在
Controller或MiddleWare中直接赋值$this->requestId
Context::set() 和 Context::get() 怎么用才不踩坑
协程上下文(Hyperf\Context\Context)是当前协程专属的“小抽屉”,键值对绑定在协程 ID 上,天然隔离。但必须注意使用时机和范围:
- 必须在协程启动后、数据需要前调用
Context::set(),比如在中间件中设置用户信息:Context::set('user', $user) - 取值时务必传默认值:
Context::get('user', null),避免未 set 就 get 导致null或警告 - 父子协程不自动继承上下文,子协程需显式复制:
go(function () use ($context) { Context::copy($context); ... }); - 不要用
Context::set()存大量业务数据(如整个订单对象),只放轻量、高频、请求级元数据(request_id、user_id、trace_id)
Request 对象属性 vs Context,该选哪个
优先用 $request->withAttribute() 和 $request->getAttribute(),而不是 Context,原因很实在:
-
$request本身是 PSR-7 标准对象,生命周期与请求强绑定,中间件链天然传递,无需手动 copy -
Context更适合跨非 HTTP 场景(如 RPC 调用、定时任务、异步队列)存上下文数据 - 若你在 Controller 里写了
$this->request->getAttribute('user'),它一定来自上层中间件,不会串;但Context::get('user')若漏了 set,就拿不到 - 混合使用时注意:Swoole 的
$request在onRequest回调里已被自动绑定到 Context(Context::set('swoole.request', $request)),所以你也可以通过 Context 拿到它,但没必要绕路
怎么快速扫描项目里有没有危险的成员变量写法
运行这条命令,能筛出大部分高风险代码:
grep -r "\$this->[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*=" app/ --include="*.php" | grep -v "function\|public\|protected\|private\|interface\|abstract"
重点关注结果中出现在 Service、Controller、Middleware 类里的赋值语句。另外,检查是否用了以下模式:
- 类里声明了
protected $data = [];且在方法中直接修改 - 有
__construct()初始化了可变状态(如连接、token) - 用了
static变量做缓存或计数
真正麻烦的不是发现它们,而是改完之后忘了清理旧逻辑——比如把 $this->cache 改成 Context::get('cache'),但没删掉原来的初始化和清空逻辑,反而引发二次污染。











