1.优化ws参数格式auth-token和user-token
This commit is contained in:
@@ -71,16 +71,20 @@ class Live extends Backend
|
||||
$adminId = $this->auth ? (int) ($this->auth->id ?? 0) : 0;
|
||||
$adminWs = GameWebSocketAuthHelper::issueAdminWsToken($adminId);
|
||||
$baseWsUrl = WebSocketConfigHelper::wsUrl($request);
|
||||
$wsUrl = WebSocketConfigHelper::appendTokensToWsUrl($baseWsUrl, [
|
||||
'admin_ws_token' => (string) ($adminWs['token'] ?? ''),
|
||||
]);
|
||||
$wsUrl = GameWebSocketAuthHelper::buildAdminConnectUrl(
|
||||
$baseWsUrl,
|
||||
(string) ($adminWs['token'] ?? '')
|
||||
);
|
||||
|
||||
return $this->success('', [
|
||||
'name' => 'ws.admin.live',
|
||||
'ws_url' => $wsUrl,
|
||||
'ws_base_url' => $baseWsUrl,
|
||||
'admin_ws_token' => (string) ($adminWs['token'] ?? ''),
|
||||
'admin-ws-token' => (string) ($adminWs['token'] ?? ''),
|
||||
'admin_ws_token_ttl' => (int) ($adminWs['ttl'] ?? 0),
|
||||
'ws_query_params' => [
|
||||
GameWebSocketAuthHelper::QUERY_ADMIN_WS_TOKEN,
|
||||
],
|
||||
'connect_tip' => 'The admin live page auto-subscribes topics for status, draw result and payout events.',
|
||||
'subscribe_topics' => $topics,
|
||||
'sample_messages' => [
|
||||
|
||||
@@ -47,16 +47,20 @@ class GameCurrentStatus extends Backend
|
||||
$adminId = $this->auth ? (int) ($this->auth->id ?? 0) : 0;
|
||||
$adminWs = GameWebSocketAuthHelper::issueAdminWsToken($adminId);
|
||||
$baseWsUrl = WebSocketConfigHelper::wsUrl($request);
|
||||
$wsUrl = WebSocketConfigHelper::appendTokensToWsUrl($baseWsUrl, [
|
||||
'admin_ws_token' => (string) ($adminWs['token'] ?? ''),
|
||||
]);
|
||||
$wsUrl = GameWebSocketAuthHelper::buildAdminConnectUrl(
|
||||
$baseWsUrl,
|
||||
(string) ($adminWs['token'] ?? '')
|
||||
);
|
||||
|
||||
return $this->success('', [
|
||||
'name' => 'ws.period',
|
||||
'ws_url' => $wsUrl,
|
||||
'ws_base_url' => $baseWsUrl,
|
||||
'admin_ws_token' => (string) ($adminWs['token'] ?? ''),
|
||||
'admin-ws-token' => (string) ($adminWs['token'] ?? ''),
|
||||
'admin_ws_token_ttl' => (int) ($adminWs['ttl'] ?? 0),
|
||||
'ws_query_params' => [
|
||||
GameWebSocketAuthHelper::QUERY_ADMIN_WS_TOKEN,
|
||||
],
|
||||
'connect_tip' => '连接成功后将自动订阅下列主题。真实业务仅在有玩家下注/结算时推送赔率;本页联调会在订阅后额外推送带 is_test/preview 的演示帧(见下方测试玩家赔率)。',
|
||||
'subscribe_topics' => $subscribeTopics,
|
||||
'odds_push_topics' => $oddsPushTopics,
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\admin;
|
||||
|
||||
use app\common\service\GameWebSocketAuthHelper;
|
||||
use Webman\Http\Request;
|
||||
|
||||
final class WebSocketConfigHelper
|
||||
@@ -41,32 +42,97 @@ final class WebSocketConfigHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* 在基础 ws_url 上拼接握手鉴权 Query:
|
||||
* - 后台用:auth_token + admin_ws_token(可观测全量主题,无 user_id 过滤)
|
||||
* - H5 用:调用方传 user_token;与 auth_token 一起拼上去
|
||||
* 在基础 ws_url 上拼接握手鉴权 Query(**输出统一为连字符**:auth-token、user-token、admin-ws-token)。
|
||||
*
|
||||
* @param array{auth_token?: string, user_token?: string, admin_ws_token?: string} $tokens
|
||||
* $tokens 的键可使用标准名或旧名下划线别名(auth_token 等),拼到 URL 时一律转为连字符。
|
||||
*
|
||||
* @param array<string, string> $tokens 如 auth-token、user-token、admin-ws-token
|
||||
* @param array<string, string> $extraQuery 如 device_id、lang
|
||||
*/
|
||||
public static function appendTokensToWsUrl(string $wsUrl, array $tokens): string
|
||||
public static function appendTokensToWsUrl(string $wsUrl, array $tokens, array $extraQuery = []): string
|
||||
{
|
||||
$wsUrl = trim($wsUrl);
|
||||
if ($wsUrl === '') {
|
||||
return $wsUrl;
|
||||
}
|
||||
$pairs = [];
|
||||
foreach (['auth_token', 'user_token', 'admin_ws_token'] as $key) {
|
||||
$val = isset($tokens[$key]) && is_string($tokens[$key]) ? trim($tokens[$key]) : '';
|
||||
if ($val !== '') {
|
||||
$pairs[] = $key . '=' . rawurlencode($val);
|
||||
|
||||
$authVal = self::pickTokenFromMap($tokens, [
|
||||
GameWebSocketAuthHelper::QUERY_AUTH_TOKEN,
|
||||
'auth_token',
|
||||
'authToken',
|
||||
]);
|
||||
$userVal = self::pickTokenFromMap($tokens, [
|
||||
GameWebSocketAuthHelper::QUERY_USER_TOKEN,
|
||||
'user_token',
|
||||
'userToken',
|
||||
'token',
|
||||
]);
|
||||
$adminVal = self::pickTokenFromMap($tokens, [
|
||||
GameWebSocketAuthHelper::QUERY_ADMIN_WS_TOKEN,
|
||||
'admin_ws_token',
|
||||
'adminWsToken',
|
||||
]);
|
||||
|
||||
$canonical = [];
|
||||
if ($authVal !== '') {
|
||||
$canonical[GameWebSocketAuthHelper::QUERY_AUTH_TOKEN] = $authVal;
|
||||
}
|
||||
if ($userVal !== '') {
|
||||
$canonical[GameWebSocketAuthHelper::QUERY_USER_TOKEN] = $userVal;
|
||||
}
|
||||
if ($adminVal !== '') {
|
||||
$canonical[GameWebSocketAuthHelper::QUERY_ADMIN_WS_TOKEN] = $adminVal;
|
||||
}
|
||||
|
||||
foreach ($extraQuery as $k => $v) {
|
||||
if (!is_string($k) || $k === '') {
|
||||
continue;
|
||||
}
|
||||
if (!is_scalar($v)) {
|
||||
continue;
|
||||
}
|
||||
$s = trim((string) $v);
|
||||
if ($s !== '') {
|
||||
$canonical[$k] = $s;
|
||||
}
|
||||
}
|
||||
if ($pairs === []) {
|
||||
|
||||
if ($canonical === []) {
|
||||
return $wsUrl;
|
||||
}
|
||||
|
||||
$pairs = [];
|
||||
foreach ($canonical as $key => $val) {
|
||||
$pairs[] = $key . '=' . rawurlencode($val);
|
||||
}
|
||||
$sep = str_contains($wsUrl, '?') ? '&' : '?';
|
||||
|
||||
return $wsUrl . $sep . implode('&', $pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $tokens
|
||||
* @param list<string> $aliases
|
||||
*/
|
||||
private static function pickTokenFromMap(array $tokens, array $aliases): string
|
||||
{
|
||||
foreach ($aliases as $key) {
|
||||
if (!isset($tokens[$key])) {
|
||||
continue;
|
||||
}
|
||||
$val = $tokens[$key];
|
||||
if (!is_string($val)) {
|
||||
continue;
|
||||
}
|
||||
$s = trim($val);
|
||||
if ($s !== '') {
|
||||
return $s;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function isLoopbackWsUrl(string $url): bool
|
||||
{
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
@@ -12,11 +12,10 @@ use Throwable;
|
||||
/**
|
||||
* WebSocket 握手鉴权助手(与 HTTP §1.3 对齐):
|
||||
*
|
||||
* 两种合法身份:
|
||||
* 1) **mobile(H5/移动端)**:URL Query 必须带 `auth_token` + `user_token`,校验通过后绑定 user_id;
|
||||
* 两种合法身份(URL Query 参数名统一为连字符,与 HTTP 请求头一致):
|
||||
* 1) **mobile(H5/移动端)**:必须带 **`auth-token`** + **`user-token`**,校验通过后绑定 user_id;
|
||||
* 分发器对 user 级主题(bet.win 等)按 user_id 过滤,只发本人。
|
||||
* 2) **admin(后台联调/实时对局页)**:URL Query 必须带 `auth_token` + `admin_ws_token`;
|
||||
* `admin_ws_token` 由后台 `wsConfig` 接口签发并写入 Redis(短时签名)。绑定 user_id=0,
|
||||
* 2) **admin(后台联调/实时对局页)**:必须带 **`admin-ws-token`**(由后台 `wsConfig` 签发,写 Redis 短时签名)。绑定 user_id=0,
|
||||
* 分发器对该连接不做 user 级过滤,可观测全量推送(用于运维/联调)。
|
||||
*
|
||||
* 任一身份通过即可建连;都不满足则拒绝握手。
|
||||
@@ -35,7 +34,23 @@ use Throwable;
|
||||
*/
|
||||
final class GameWebSocketAuthHelper
|
||||
{
|
||||
/** admin_ws_token 在 Redis 中的 key 前缀;value 存 admin_id,TTL 由 issueAdminWsToken 决定 */
|
||||
/** WebSocket 握手 Query 标准参数名(与 HTTP 头 auth-token / user-token 一致,一律用连字符) */
|
||||
public const QUERY_AUTH_TOKEN = 'auth-token';
|
||||
|
||||
public const QUERY_USER_TOKEN = 'user-token';
|
||||
|
||||
public const QUERY_ADMIN_WS_TOKEN = 'admin-ws-token';
|
||||
|
||||
/** @var list<string> 兼容旧客户端的下划线/驼峰别名(解析时仍可读,拼 URL 时勿用) */
|
||||
private const LEGACY_AUTH_KEYS = ['auth_token', 'authToken'];
|
||||
|
||||
/** @var list<string> */
|
||||
private const LEGACY_USER_KEYS = ['user_token', 'userToken', 'token'];
|
||||
|
||||
/** @var list<string> */
|
||||
private const LEGACY_ADMIN_KEYS = ['admin_ws_token', 'adminWsToken'];
|
||||
|
||||
/** admin-ws-token 在 Redis 中的 key 前缀;value 存 admin_id,TTL 由 issueAdminWsToken 决定 */
|
||||
private const ADMIN_TOKEN_REDIS_PREFIX = 'dfw:v1:ws:admin_token:';
|
||||
private const ADMIN_TOKEN_DEFAULT_TTL = 7200;
|
||||
|
||||
@@ -45,9 +60,9 @@ final class GameWebSocketAuthHelper
|
||||
*/
|
||||
public static function authorize(array $query): array
|
||||
{
|
||||
$authToken = self::pickFirstString($query, ['auth_token', 'auth-token', 'authToken']);
|
||||
$userToken = self::pickFirstString($query, ['user_token', 'user-token', 'userToken', 'token']);
|
||||
$adminWsToken = self::pickFirstString($query, ['admin_ws_token', 'admin-ws-token', 'adminWsToken']);
|
||||
$authToken = self::pickFirstString($query, array_merge([self::QUERY_AUTH_TOKEN], self::LEGACY_AUTH_KEYS));
|
||||
$userToken = self::pickFirstString($query, array_merge([self::QUERY_USER_TOKEN], self::LEGACY_USER_KEYS));
|
||||
$adminWsToken = self::pickFirstString($query, array_merge([self::QUERY_ADMIN_WS_TOKEN], self::LEGACY_ADMIN_KEYS));
|
||||
|
||||
// ===== Admin 旁路:只校验 admin_ws_token(由后台 wsConfig 签发,已隐含管理员身份) =====
|
||||
if ($adminWsToken !== '') {
|
||||
@@ -152,6 +167,29 @@ final class GameWebSocketAuthHelper
|
||||
return $adminId === false ? 0 : (int) $adminId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼装移动端 WebSocket 连接 URL(Query 固定为 auth-token、user-token)。
|
||||
*
|
||||
* @param array<string, string> $extraQuery 其它 Query,如 device_id、lang
|
||||
*/
|
||||
public static function buildMobileConnectUrl(string $baseWsUrl, string $authToken, string $userToken, array $extraQuery = []): string
|
||||
{
|
||||
return \app\common\library\admin\WebSocketConfigHelper::appendTokensToWsUrl($baseWsUrl, [
|
||||
self::QUERY_AUTH_TOKEN => $authToken,
|
||||
self::QUERY_USER_TOKEN => $userToken,
|
||||
], $extraQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼装后台 WebSocket 连接 URL(Query 固定为 admin-ws-token)。
|
||||
*/
|
||||
public static function buildAdminConnectUrl(string $baseWsUrl, string $adminWsToken): string
|
||||
{
|
||||
return \app\common\library\admin\WebSocketConfigHelper::appendTokensToWsUrl($baseWsUrl, [
|
||||
self::QUERY_ADMIN_WS_TOKEN => $adminWsToken,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ws header 中解析 GET 行 Query(Workerman 在 onWebSocketConnect($connection, $request) 时
|
||||
* $request 可能为字符串或对象;为兼容,这里允许直接传 URI Query 字符串)。
|
||||
|
||||
@@ -21,8 +21,8 @@ use Workerman\Timer;
|
||||
* 设计与 docs/36字花-移动端接口设计草案.md §7 对齐:
|
||||
*
|
||||
* - 握手鉴权(GameWebSocketAuthHelper)
|
||||
* - mobile:URL Query `auth_token` + `user_token`,绑定 user_id,user 级主题按 user_id 过滤
|
||||
* - admin: URL Query `admin_ws_token`(后台 wsConfig 签发,写 Redis 短时签名),user_id=0,
|
||||
* - mobile:URL Query **`auth-token`** + **`user-token`**(与 HTTP 头一致),绑定 user_id
|
||||
* - admin: URL Query **`admin-ws-token`**(后台 wsConfig 签发),user_id=0,
|
||||
* user 级主题不过滤(运维/联调可观测全量)
|
||||
* - 客户端 -> 服务端:`{"action":"ping"}` / `{"action":"subscribe","topics":[...]}`
|
||||
* - 服务端 -> 客户端:`ws.connected` / `ws.subscribed` / `pong` / `ws.error` / 业务事件帧
|
||||
|
||||
Reference in New Issue
Block a user