Skip to content

Latest commit

 

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

digood/sso

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_inCache::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') 隔离,避免污染真实缓存。

About

SSO单点登录

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages