Digood SSO 服务包(Laravel)。提供企业微信 / Logto 单点登录、会话管理,以及 流式授权 Token(供第三方站点无 Cookie 消费 SSE 流式接口)。
- 要求:PHP
^8.1、Laravel(illuminate/framework) - 自动发现:
Digood\Sso\SsoServiceProvider(composer extra.laravel.providers)
composer require digood/sso:^1.1.4宿主应用把 AI 编辑器的 SSE 流式接口 URL(如 /editor/chat/stream/{guid})交给第三方站点前端,第三方用 fetch(url, { credentials: 'omit' }) 消费——不带任何 Cookie。因此不能依赖 session/cookie 鉴权,改为在 URL 中携带一次性签发的流式 token。
与
sso_api_set_temporary_sign_in/sso_api_read_temporary_sign_in(Cache::pull一次性语义)不同,流式 token 用Cache::get验证不消费——EventSource 断线后拿同一 URL 自动重连,TTL 内反复验证都能通过。
// 1. 铸造 Token(宿主创建会话时)
$key = sso_api_set_stream_token(
['guid' => $sessionGuid, 'user_id' => $userId], // 调用方自定义,建议绑定 guid + user_id
30 // TTL 分钟,默认 30
);
// 成功返回完整 key(形如 sso_api_stream_token_<uuid>),失败返回 false
// 2. 验证 Token(不消费,TTL 内可重复调用)
$payload = sso_api_read_stream_token($key);
// => 数组 payload 有效
// => null 无效 / 过期 / 已吊销
// 3. 吊销 Token(会话结束主动作废)
sso_api_revoke_stream_token($key);缓存 key 一律以 md5($key) 存储、对外返回原文 key(与现有 sso_api_temporary_sign_in_* 约定一致)。
SsoStreamTokenMiddleware 供宿主路由叠挂,实现「带 token 走流通道、不带 token 走常规登录」的双通道:
use Digood\Sso\Http\Middleware\SsoStreamTokenMiddleware;
Route::middleware([SsoStreamTokenMiddleware::class, SsoMiddleware::class])
->prefix('editor/chat')
->group(function () {
Route::get('/stream/{guid}', [EditorController::class, 'stream']);
});
// 自定义参数名 / 绑定键:
// Route::get('/stream/{guid}', ...)->middleware(SsoStreamTokenMiddleware::class . ':sso_temporary_token,guid');行为(handle 参数:$param = 'sso_temporary_token',$bindKey = 'guid'):
| 情形 | 结果 |
|---|---|
| URL 无 token 参数 | 放行(交给后续 SsoMiddleware 常规登录) |
| 有 token 但无效 / 过期 / 非数组 payload | 403 Forbidden(text/plain) |
token 有效但 payload[$bindKey] 与路由参数严格不等 |
403(防 token 挪用) |
| 校验通过 | 若 payload 含 user_id,注入 $request->attributes->set('sso_user_id', ...) 后放行 |
中间件不种 session、不写 cookie——消费方不收 cookie,种了无效。
// EditorController::create() —— 铸 token 并拼入流式 URL
$streamToken = sso_api_set_stream_token(
['guid' => $session->guid, 'user_id' => $session->user_id],
30
);
$session->url_stream = route('editor.chat.stream', [
'guid' => $session->guid,
'sso_temporary_token' => $streamToken,
]);
// EditorController::stream() —— 读取注入的归属用户
public function stream(Request $request, $guid)
{
$userId = $request->attributes->get('sso_user_id');
// ... SSE 流式输出
}
// 记日志不再依赖 session:
// logTokenUsage($request->attributes->get('sso_user_id'));const es = new EventSource('/editor/chat/stream/abc?sso_temporary_token=xxx');
es.onmessage = (e) => { /* 处理 */ };
es.onerror = () => { /* 自动重连——同一 URL 同一 token,后端不消费,重连仍通过 */ };仓库根提供 verify_stream_token.php(tinker 管道脚本),宿主环境可一键验证全部行为:
# Windows
Get-Content verify_stream_token.php | php artisan tinker
# Unix / macOS
cat verify_stream_token.php | php artisan tinker输出 ALL CHECKS PASSED 即验证通过(覆盖 mint / 非消费双读 / blank 守卫无 md5 deprecation / revoke / TTL 接线)。
以下骨架用于宿主仓库(已引入 PHPUnit/Pest 的 Laravel 应用),覆盖流式 token 全链路;复制到宿主 tests/Feature/ 后按实际路由/模型调整:
<?php
use Digood\Sso\Http\Middleware\SsoStreamTokenMiddleware;
use Illuminate\Support\Facades\Cache;
it('stream 路由:无 token 走常规通道', function () {
$this->get('/editor/chat/stream/guid-abc')
->assertStatus(401); // 或宿主 SsoMiddleware 的未登录行为
});
it('stream 路由:无效 token 返回 403', function () {
$this->get('/editor/chat/stream/guid-abc?sso_temporary_token=bad-token')
->assertStatus(403);
});
it('stream 路由:有效 token 绑定匹配则放行并注入归属', function () {
$key = sso_api_set_stream_token(['guid' => 'guid-abc', 'user_id' => 42], 30);
$this->get('/editor/chat/stream/guid-abc?sso_temporary_token=' . $key)
->assertOk(); // 宿主控制器返回 200 表示进入
// 若控制器需读归属用户:$request->attributes->get('sso_user_id')
});
it('stream 路由:有效 token 但绑定不符返回 403(防挪用)', function () {
$key = sso_api_set_stream_token(['guid' => 'guid-abc', 'user_id' => 42], 30);
$this->get('/editor/chat/stream/guid-OTHER?sso_temporary_token=' . $key)
->assertStatus(403);
});
it('stream 路由:吊销后同一 URL 返回 403', function () {
$key = sso_api_set_stream_token(['guid' => 'guid-abc', 'user_id' => 42], 30);
sso_api_revoke_stream_token($key);
$this->get('/editor/chat/stream/guid-abc?sso_temporary_token=' . $key)
->assertStatus(403);
});
it('helper:TTL 内可重复验证(SSE 断线重连语义)', function () {
$key = sso_api_set_stream_token(['guid' => 'g', 'user_id' => 1], 30);
expect(sso_api_read_stream_token($key))->toBe(['guid' => 'g', 'user_id' => 1]);
expect(sso_api_read_stream_token($key))->toBe(['guid' => 'g', 'user_id' => 1]); // 不消费
expect(sso_api_read_stream_token($key))->toBe(['guid' => 'g', 'user_id' => 1]); // 第三次仍有效
});中间件按 FQCN 叠挂(
SsoStreamTokenMiddleware::class),宿主无需在包内注册别名;测试建议用Cache::store('array')隔离,避免污染真实缓存。