digood / sso
Digood SSO Service
Requires
- php: ^8.1
- illuminate/http: *
- illuminate/support: *
- laravel/framework: *
- logto/sdk: ^0.3.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
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
流式授权 Token(SSE 场景)
背景
宿主应用把 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 内反复验证都能通过。
API
// 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 接线)。
宿主侧 Feature Test 骨架
以下骨架用于宿主仓库(已引入 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')隔离,避免污染真实缓存。