diff --git a/.env-example b/.env-example index 7b69736..941dd04 100644 --- a/.env-example +++ b/.env-example @@ -34,8 +34,8 @@ AGENT_AUTH_JWT_SECRET= PLAYX_SESSION_EXPIRE_SECONDS=3600 # verifyToken 是否仅本地联调(false=走对方远程校验) PLAYX_VERIFY_TOKEN_LOCAL_ONLY=false -# verifyToken 对方接口路径 -PLAYX_TOKEN_VERIFY_URL=/api/v1/auth/verify-token +# verifyToken:填完整 https URL 时仅发 request_id+token;填相对路径时拼 PLAYX_ANGPOW_IMPORT_BASE_URL 并发商户签名体 +PLAYX_TOKEN_VERIFY_URL=https://callback-mallsys.superior3.net/callback/api/mallsys/plx/auth/verify-token # verifyToken 仅本地联调时的默认用户ID PLAYX_VERIFY_TOKEN_LOCAL_DEFAULT_USER_ID=testmyr # verifyToken 仅本地联调时的默认用户名 diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index f9ca347..399b294 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -411,56 +411,85 @@ class Playx extends Api return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } - $baseUrl = config('playx.angpow_import.base_url', ''); - $verifyUrl = config('playx.api.token_verify_url', '/api/v1/auth/verify-token'); - $verifyPath = ltrim(strval($verifyUrl), '/'); - if ($verifyPath === '') { - return $this->error(__('PlayX API not configured')); - } - if ($baseUrl === '') { - return $this->error(__('PlayX API not configured')); + $baseUrl = strval(config('playx.angpow_import.base_url', '')); + $verifyUrlRaw = strval(config('playx.api.token_verify_url', '/api/v1/auth/verify-token')); + $verifyUrlTrimmed = trim($verifyUrlRaw); + $isAbsoluteVerifyUrl = str_starts_with($verifyUrlTrimmed, 'http://') + || str_starts_with($verifyUrlTrimmed, 'https://'); + + if ($isAbsoluteVerifyUrl) { + $targetVerifyUrl = $verifyUrlTrimmed; + } else { + $verifyPath = ltrim($verifyUrlTrimmed, '/'); + if ($verifyPath === '') { + return $this->error(__('PlayX API not configured')); + } + if ($baseUrl === '') { + return $this->error(__('PlayX API not configured')); + } + $targetVerifyUrl = rtrim($baseUrl, '/') . '/' . $verifyPath; } try { - $merchantCode = strval(config('playx.angpow_import.merchant_code', '')); - $authKey = strval(config('playx.angpow_import.auth_key', '')); - if ($merchantCode === '' || $authKey === '') { - return $this->error(__('PlayX API not configured')); - } - $requestId = 'mall_' . uniqid(); - $requestDate = strval(time()); - $signatureInput = 'merchant_code=' . $merchantCode - . '&request_date=' . $requestDate - . '&request_id=' . $requestId - . '&token=' . $token; - $signature = $this->buildPlayxTokenVerifySignature($signatureInput, $authKey); - if ($signature === null) { - return $this->error(__('Invalid signature'), null, 0, ['statusCode' => 500]); - } - $client = new \GuzzleHttp\Client([ - 'base_uri' => rtrim($baseUrl, '/') . '/', - 'timeout' => 10, + $clientOptions = [ + 'timeout' => 10, 'http_errors' => false, - ]); - $headers = [ - 'Content-Type' => 'application/json', - 'X-Request-Signature' => $signature, - 'X-Signature' => $signature, - 'X-Request-Date' => $requestDate, - 'X-Request-ID' => $requestId, ]; - $payload = [ - 'merchant_code' => $merchantCode, - 'request_date' => $requestDate, - 'request_id' => $requestId, - 'token' => $token, - ]; - $res = $client->post($verifyPath, [ - 'headers' => $headers, - 'json' => $payload, - ]); + if (!$isAbsoluteVerifyUrl) { + $clientOptions['base_uri'] = rtrim($baseUrl, '/') . '/'; + } + $client = new \GuzzleHttp\Client($clientOptions); + + if ($isAbsoluteVerifyUrl) { + $headers = [ + 'Content-Type' => 'application/json', + ]; + $payload = [ + 'request_id' => $requestId, + 'token' => $token, + ]; + $res = $client->post($targetVerifyUrl, [ + 'headers' => $headers, + 'json' => $payload, + ]); + } else { + $merchantCode = strval(config('playx.angpow_import.merchant_code', '')); + $authKey = strval(config('playx.angpow_import.auth_key', '')); + if ($merchantCode === '' || $authKey === '') { + return $this->error(__('PlayX API not configured')); + } + + $requestDate = strval(time()); + $signatureInput = 'merchant_code=' . $merchantCode + . '&request_date=' . $requestDate + . '&request_id=' . $requestId + . '&token=' . $token; + $signature = $this->buildPlayxTokenVerifySignature($signatureInput, $authKey); + if ($signature === null) { + return $this->error(__('Invalid signature'), null, 0, ['statusCode' => 500]); + } + + $headers = [ + 'Content-Type' => 'application/json', + 'X-Request-Signature' => $signature, + 'X-Signature' => $signature, + 'X-Request-Date' => $requestDate, + 'X-Request-ID' => $requestId, + ]; + $payload = [ + 'merchant_code' => $merchantCode, + 'request_date' => $requestDate, + 'request_id' => $requestId, + 'token' => $token, + ]; + $verifyPath = ltrim($verifyUrlTrimmed, '/'); + $res = $client->post($verifyPath, [ + 'headers' => $headers, + 'json' => $payload, + ]); + } $data = json_decode(strval($res->getBody()), true); $code = $res->getStatusCode(); if ($code !== 200 || empty($data['user_id'])) { @@ -477,6 +506,9 @@ class Playx extends Api $remoteMsg = mb_substr($bodyText, 0, 300); } } + if ($remoteMsg === '' || str_contains(strtolower($remoteMsg), 'error($msg, null, 0, ['statusCode' => 401]); diff --git a/config/playx.php b/config/playx.php index f35e0af..22c9ed1 100644 --- a/config/playx.php +++ b/config/playx.php @@ -32,6 +32,7 @@ return [ 'api' => [ 'base_url' => strval(env('PLAYX_API_BASE_URL', '')), 'secret_key' => strval(env('PLAYX_API_SECRET_KEY', '')), + // 完整 https URL:回调校验,Body 仅 request_id + token;相对路径:拼 angpow_import.base_url + 商户签名字段 'token_verify_url' => strval(env('PLAYX_TOKEN_VERIFY_URL', '/api/v1/auth/verify-token')), 'bonus_grant_url' => '/api/v1/bonus/grant', 'balance_credit_url' => '/api/v1/balance/credit', diff --git a/docs/PlayX-对接文档(积分商城).md b/docs/PlayX-对接文档(积分商城).md index 2dd5d93..9e14141 100644 --- a/docs/PlayX-对接文档(积分商城).md +++ b/docs/PlayX-对接文档(积分商城).md @@ -71,15 +71,18 @@ flowchart LR 1. 用户在 playX 内打开积分商城入口(iframe)。 2. playX 前端通过 postMessage 将 **playX 下发的 token**(及必要上下文)传给商城 H5。 -3. 商城 H5 调用商城后端 **`POST /api/v1/mall/verifyToken`**(前端只传 `token`),由商城后端向 playX 的 **Token Verification API**(`PLAYX_ANGPOW_IMPORT_BASE_URL` + `PLAYX_TOKEN_VERIFY_URL`)发起校验。 -4. **前提**:配置 **`playX.verify_token_local_only = false`**,且 **`PLAYX_ANGPOW_IMPORT_BASE_URL`** 已配置为可访问的 playX 基地址。 +3. 商城 H5 调用商城后端 **`POST /api/v1/mall/verifyToken`**(前端只传 `token`),由商城后端向 **Token Verification API** 发起校验。 +4. **前提**:配置 **`playX.verify_token_local_only = false`**,且 **`PLAYX_TOKEN_VERIFY_URL`** 已配置: + - **完整 `https://...` URL**(如回调:`https://callback-mallsys.superior3.net/callback/api/mallsys/plx/auth/verify-token`):商城对该地址 `POST`,Body 仅 **`request_id` + `token`**,无需 `PLAYX_ANGPOW_IMPORT_BASE_URL`。 + - **相对路径**(如 `/api/v1/auth/verify-token`):需同时配置 **`PLAYX_ANGPOW_IMPORT_BASE_URL`**,并按商户约定附带 **`merchant_code` / `request_date` / `X-Request-Signature`** 等。 5. playX 返回 **`user_id`、`username`**(及可选会话过期时间等)。 6. 商城写入 **`mall_playx_session`**(`session_id` + 上述 `user_id`/`username` + 过期时间),后续 H5 可用 **`session_id`** 或 **`token`(商城临时 token,见模式 B)** 调用资产/领取等接口。 幂等与安全: - H5 **不要**把 playX 的 `user_id` 当作唯一可信凭据直传下单;**以 token 换 session** 或由商城签发 token 的流程为准。 -- playX 侧 Token Verification API 的鉴权/签名由商城后端完成(`merchant_code/request_date/request_id` + `X-Request-Signature`);H5 不参与签名计算。 +- 若 `PLAYX_TOKEN_VERIFY_URL` 为**完整 https URL**:对端按回调文档校验,商城后端只组 **`request_id` + `token`**,H5 不参与。 +- 若为**相对路径 + 基地址**商户网关:鉴权/签名由商城后端完成(`merchant_code/request_date/request_id` + `X-Request-Signature`);H5 不参与签名计算。 #### 4.1.3 模式 B:本地 / 无 playX 环境(商城自校验,不请求 playX) @@ -211,26 +214,46 @@ flowchart LR ### 5.2 Mall → playX:Token Verification API -- **目的**:商城后端校验 token/session,获取可信 `user_id` 与 `username`。 +- **目的**:商城后端校验 token,获取可信 `user_id` 与 `username`。 - **Method**:`POST` -- **Base URL**:`PLAYX_ANGPOW_IMPORT_BASE_URL`(当前联调:`https://plx-uat2.ttwd3.com/en-my/mall`) -- **Path**:`/api/v1/auth/verify-token` -- **完整 URL**:`{PLAYX_ANGPOW_IMPORT_BASE_URL}/api/v1/auth/verify-token` -- **签名 Header**:`X-Request-Signature`(`Base64(HMAC-SHA1(canonical, key))`) -- **签名明文 canonical**:`merchant_code={merchant_code}&request_date={request_date}&request_id={request_id}&token={token}` -- **request_date 格式**:Unix 时间戳字符串(秒) +- **Content-Type**:`application/json` -请求字段说明(当前实现): +#### 5.2.1 回调网关(推荐当前联调) + +- **完整 URL**:由环境变量 **`PLAYX_TOKEN_VERIFY_URL`** 填完整地址,例如: + `https://callback-mallsys.superior3.net/callback/api/mallsys/plx/auth/verify-token` +- **请求 Body**: | 字段名 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | -| `merchant_code` | String | 是 | 商户编码,默认 `plx`。 | -| `request_date` | String | 是 | 请求时间戳(秒)。 | -| `request_id` | String | 是 | 商城系统生成的唯一请求流水号。 | -| `token` | String | 是 | 从带有商城的 Iframe `postMessage` 接收到的用户登录凭证。 | +| `request_id` | String | 是 | 商城生成的请求追踪号。 | +| `token` | String | 是 | 前端传入的 playX 临时凭证。 | 请求示例: +```json +{ + "request_id": "mall_20260319_9f1b6d", + "token": "eyJhbGciOi..." +} +``` + +#### 5.2.2 商户网关(相对路径 + HMAC) + +- **Base URL**:`PLAYX_ANGPOW_IMPORT_BASE_URL` +- **Path**:`PLAYX_TOKEN_VERIFY_URL`(相对路径,如 `/api/v1/auth/verify-token`) +- **完整 URL**:`{PLAYX_ANGPOW_IMPORT_BASE_URL}{PLAYX_TOKEN_VERIFY_URL}` +- **签名 Header**:`X-Request-Signature`(`Base64(HMAC-SHA1(canonical, key))`) +- **签名明文 canonical**:`merchant_code={merchant_code}&request_date={request_date}&request_id={request_id}&token={token}` +- **request_date**:Unix 时间戳字符串(秒) + +| 字段名 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `merchant_code` | String | 是 | 商户编码。 | +| `request_date` | String | 是 | 请求时间戳(秒)。 | +| `request_id` | String | 是 | 商城系统生成的唯一请求流水号。 | +| `token` | String | 是 | 用户登录凭证。 | + ```json { "merchant_code": "plx", diff --git a/docs/PlayX-调用积分商城接口说明.md b/docs/PlayX-调用积分商城接口说明.md index b5e4d03..c7eac32 100644 --- a/docs/PlayX-调用积分商城接口说明.md +++ b/docs/PlayX-调用积分商城接口说明.md @@ -299,8 +299,10 @@ curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \ **前端入参约定(重要)**: - 前端/客户端调用本接口时,**只需要传 `token`**(或兼容传 `session`)。 -- `merchant_code`、`request_date`、`request_id`、`X-Request-Signature` 由商城后端生成并带给 playX,前端无需参与签名。 -- 若仅传 `token` 仍校验失败,通常是 token 本身无效/过期或对方网关路径未放通,并非前端少传字段。 +- 商城后端调用 playX 校验时: + - 若 **`PLAYX_TOKEN_VERIFY_URL` 为完整 `https://` URL**(回调网关):后端只向对方发送 **`request_id` + `token`**(与对端文档一致),无需前端传商户字段。 + - 若为**相对路径**:后端会拼 **`PLAYX_ANGPOW_IMPORT_BASE_URL`**,并生成 **`merchant_code` / `request_date` / `request_id` / `X-Request-Signature`**,前端仍不参与签名。 +- 若仅传 `token` 仍校验失败,多为 token 无效/过期或上游 URL 未放通,并非前端少传字段。 **请求示例:** ```json @@ -630,10 +632,10 @@ GET /api/v1/mall/addressList?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66 |-----------------|------| | `PLAYX_DAILY_PUSH_SECRET` | 非空则 Daily Push 必须带合法 HMAC 头 | | `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verifyToken 不请求 playX 远程 | -| `PLAYX_ANGPOW_IMPORT_BASE_URL` | 远程 `verify-token` 基础地址(当前联调可含站点前缀路径) | -| `PLAYX_TOKEN_VERIFY_URL` | 远程 `verify-token` 路径(默认 `/api/v1/auth/verify-token`) | -| `PLAYX_ANGPOW_MERCHANT_CODE` | `verify-token` 请求体 `merchant_code` | -| `PLAYX_ANGPOW_IMPORT_AUTH_KEY` | `verify-token` 签名密钥(HMAC-SHA1,Base64 输出) | +| `PLAYX_ANGPOW_IMPORT_BASE_URL` | Angpow 推送等接口基地址;**相对路径** `verify-token` 时亦作其基地址 | +| `PLAYX_TOKEN_VERIFY_URL` | **完整 `https://` URL**:回调校验,Body 仅 `request_id`+`token`;**相对路径**:拼基地址 + 商户签名体 | +| `PLAYX_ANGPOW_MERCHANT_CODE` | 仅相对路径 `verify-token` 时使用 | +| `PLAYX_ANGPOW_IMPORT_AUTH_KEY` | 仅相对路径 `verify-token` 时 HMAC 密钥 | ---