优化Token验证接口

This commit is contained in:
2026-05-06 10:18:26 +08:00
parent 05f2d6f084
commit 04680408e6
11 changed files with 59 additions and 31 deletions

View File

@@ -34,7 +34,7 @@ AGENT_AUTH_JWT_SECRET=
PLAYX_SESSION_EXPIRE_SECONDS=3600
# verifyToken 是否仅本地联调false=走对方远程校验)
PLAYX_VERIFY_TOKEN_LOCAL_ONLY=false
# verifyToken完整 https URL 时 X-Request-Signature 与 angpow-imports 同源Body 含 merchant_code/report_date/request_id/token相对路径时拼基址且 Body 用 request_date
# verifyToken完整 https URL 时签名同 angpowBody 为 request_id/request_date/token相对路径时拼基址+商户四字段
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

View File

@@ -449,10 +449,10 @@ class Playx extends Api
return $this->error(__('PlayX API not configured'));
}
// 与 angpow-imports 一致HMAC-SHA1 → Base64密钥 hex/base64 兼容;仅 X-Request-Signature
$reportDate = strval(time());
// 与 angpow-imports 同源HMAC-SHA1 → Base64仅 X-Request-SignatureBody 对齐对端必填 request_date + PlayX 文档 request_id/token
$requestDate = strval(time());
$signatureInput = 'merchant_code=' . $merchantCode
. '&report_date=' . $reportDate
. '&request_date=' . $requestDate
. '&request_id=' . $requestId
. '&token=' . $token;
$signature = $this->buildPlayxTokenVerifySignature($signatureInput, $authKey);
@@ -461,14 +461,13 @@ class Playx extends Api
}
$headers = [
'Content-Type' => 'application/json',
'X-Request-Signature' => $signature,
'Content-Type' => 'application/json',
'X-Request-Signature' => $signature,
];
$payload = [
'merchant_code' => $merchantCode,
'report_date' => $reportDate,
'request_id' => $requestId,
'token' => $token,
'request_id' => $requestId,
'request_date' => $requestDate,
'token' => $token,
];
$res = $client->post($targetVerifyUrl, [
'headers' => $headers,
@@ -527,7 +526,7 @@ class Playx extends Api
}
}
if ($remoteMsg === '' || str_contains(strtolower($remoteMsg), '<html')) {
$remoteMsg = 'PlayX verify-token failed: HTTP ' . strval($code) . ' at ' . $targetVerifyUrl;
$remoteMsg = __('PlayX verify upstream failed', [strval($code), $targetVerifyUrl]);
}
$msg = $remoteMsg !== '' ? $remoteMsg : __('Token expiration');

View File

@@ -60,6 +60,7 @@ return [
'nicknameChsDash' => 'Username may only contain letters, numbers, underscores and dashes',
'Invalid token' => 'Invalid or expired token',
'PlayX API not configured' => 'PlayX API is not configured',
'PlayX verify upstream failed' => 'Upstream token verification failed (HTTP %s): %s',
'Duplicate input' => 'Duplicate submission',
'Ok' => 'OK',
'Failed to map playx user to mall user' => 'Failed to map PlayX user to mall user',

View File

@@ -61,6 +61,7 @@ return [
'nicknameChsDash' => 'Nama pengguna hanya huruf, nombor, garis bawah dan sempang',
'Invalid token' => 'Token tidak sah atau tamat tempoh',
'PlayX API not configured' => 'API PlayX tidak dikonfigurasi',
'PlayX verify upstream failed' => 'Pengesahan token hulu gagal (HTTP %s): %s',
'Duplicate input' => 'Penghantaran pendua',
'Ok' => 'OK',
'Failed to map playx user to mall user' => 'Gagal memetakan pengguna PlayX ke pengguna mall',

View File

@@ -62,6 +62,7 @@ return [
// PlayX API v1 /api/v1/*
'Invalid token' => '令牌无效或已过期',
'PlayX API not configured' => '未配置 PlayX 接口地址',
'PlayX verify upstream failed' => '上游 Token 校验失败HTTP %s%s',
'Duplicate input' => '重复提交',
'Ok' => '成功',
'Failed to map playx user to mall user' => '无法将 PlayX 用户关联到商城用户',

View File

@@ -12,7 +12,7 @@ use Webman\Http\Response;
* 加载控制器语言包中间件Webman 迁移版,等价 ThinkPHP LoadLangPack
* 根据当前路由加载对应控制器的语言包到 Translator
*
* 对外 api/优先请求头 langzh / zh-cn → zh-cnen → enms → 马来语包),未传则 think-lang再默认 zh-cn不根据浏览器 Accept-Language
* 对外 api/语言优先级为 请求头 lang → GET/POST 参数 lang支持 zh/ZH、en/EN 等)→ think-lang再默认 zh-cn不根据浏览器 Accept-Language
* admin/think-lang → Accept-Language → 配置默认
*/
class LoadLangPack implements MiddlewareInterface
@@ -45,6 +45,16 @@ class LoadLangPack implements MiddlewareInterface
if ($langHeader !== '') {
$langSet = $this->normalizeLangHeader($langHeader, $allowLangList);
}
if ($langSet === null) {
$langParam = strval($request->get('lang', ''));
if ($langParam === '') {
$langParam = strval($request->post('lang', ''));
}
$langParam = trim($langParam);
if ($langParam !== '') {
$langSet = $this->normalizeLangHeader($langParam, $allowLangList);
}
}
}
// 与后台 Vue 一致的 think-lang对外 API 在 lang 未设置时仍可生效)

View File

@@ -32,7 +32,7 @@ return [
'api' => [
'base_url' => strval(env('PLAYX_API_BASE_URL', '')),
'secret_key' => strval(env('PLAYX_API_SECRET_KEY', '')),
// 完整 https URL回调校验X-Request-Signature 与 angpow-imports 同源;相对路径:拼 angpow_import.base_url + request_date 商户体
// 完整 https URL回调 Body 为 request_id/request_date/token签名 canonical 含 merchant_code相对路径拼基址 + 商户体
'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',

View File

@@ -230,6 +230,10 @@ Body 含 `receiver_name`(收货人,建议填写;实物兑换下单快照
- `token`
- `session`
可选 **`lang`**`zh` / `ZH` / `zh-cn` 返回中文 **`msg`**(默认中文),`en` / `EN` 返回英文可通过请求头、Query 或与本接口 Body 同传的表单字段传入。
建议 **`Content-Type: application/json`**Body 示例:`{"token":"..."}`。使用 `multipart/form-data` 时同样只传 `token`(及可选 `lang`)即可。
成功返回:
- `data.session_id`
- `data.user_id`

View File

@@ -73,7 +73,7 @@ flowchart LR
2. playX 前端通过 postMessage 将 **playX 下发的 token**(及必要上下文)传给商城 H5。
3. 商城 H5 调用商城后端 **`POST /api/v1/mall/verifyToken`**(前端只传 `token`),由商城后端向 **Token Verification API** 发起校验。
4. **前提**:配置 **`playX.verify_token_local_only = false`**,且 **`PLAYX_TOKEN_VERIFY_URL`** 已配置:
- **完整 `https://...` URL**(如回调):商城 `POST` 该地址;**`X-Request-Signature` 生成方式与 `PLAYX_ANGPOW_IMPORT_PATH`angpow-imports)一致**Body **`merchant_code``report_date``request_id``token`**;需配置 **`PLAYX_ANGPOW_MERCHANT_CODE`**、**`PLAYX_ANGPOW_IMPORT_AUTH_KEY`**;无需 `PLAYX_ANGPOW_IMPORT_BASE_URL`
- **完整 `https://...` URL**(如回调):商城 `POST` 该地址;**`X-Request-Signature` angpow-imports 同源**JSON Body **`request_id``request_date``token`**`request_date` 为对端必填);签名 canonical 含 **`merchant_code`(配置项,仅用于签名)**;需 **`PLAYX_ANGPOW_MERCHANT_CODE`**、**`PLAYX_ANGPOW_IMPORT_AUTH_KEY`**;无需 `PLAYX_ANGPOW_IMPORT_BASE_URL`
- **相对路径**(如 `/api/v1/auth/verify-token`):需同时配置 **`PLAYX_ANGPOW_IMPORT_BASE_URL`**Body 使用 **`request_date`**,并按商户约定附带签名等。
5. playX 返回 **`user_id``username`**(及可选会话过期时间等)。
6. 商城写入 **`mall_playx_session`**`session_id` + 上述 `user_id`/`username` + 过期时间),后续 H5 可用 **`session_id`** 或 **`token`(商城临时 token见模式 B** 调用资产/领取等接口。
@@ -81,7 +81,7 @@ flowchart LR
幂等与安全:
- H5 **不要**把 playX 的 `user_id` 当作唯一可信凭据直传下单;**以 token 换 session** 或由商城签发 token 的流程为准。
-`PLAYX_TOKEN_VERIFY_URL` 为**完整 https URL**`X-Request-Signature` 与 angpow-imports 同源算法Body **`merchant_code``report_date``request_id``token`**H5 不参与签名。
-`PLAYX_TOKEN_VERIFY_URL` 为**完整 https URL**`X-Request-Signature` 与 angpow-imports 同源Body **`request_id``request_date``token`**`merchant_code` 仅参与签名字符串;H5 不参与签名。
- 若为**相对路径 + 基地址**商户网关:鉴权/签名由商城后端完成(`merchant_code/request_date/request_id` + `X-Request-Signature`H5 不参与签名计算。
#### 4.1.3 模式 B本地 / 无 playX 环境(商城自校验,不请求 playX
@@ -223,23 +223,22 @@ flowchart LR
- **完整 URL**:由环境变量 **`PLAYX_TOKEN_VERIFY_URL`** 填完整地址,例如:
`https://callback-mallsys.superior3.net/callback/api/mallsys/plx/auth/verify-token`
- **请求 Header**:与 **`PLAYX_ANGPOW_IMPORT_PATH`angpow-imports** 相同,仅 **`Content-Type: application/json`**、**`X-Request-Signature`**`Base64(HMAC-SHA1(canonical, PLAYX_ANGPOW_IMPORT_AUTH_KEY))`,密钥解析规则与 angpow 一致:支持 hex / base64 / 明文)。
- **签名明文 canonical**`merchant_code={merchant_code}&report_date={report_date}&request_id={request_id}&token={token}``report_date` 为 Unix 秒时间戳字符串,与 angpow 字段命名一致)。
- **请求 Body**
- **签名明文 canonical**(与 angpow 同源算法,字段名与对端必填 `request_date` 对齐):
`merchant_code={PLAYX_ANGPOW_MERCHANT_CODE}&request_date={request_date}&request_id={request_id}&token={token}``request_date` 为 Unix 秒时间戳字符串;`merchant_code` 仅参与签名,**不写入** JSON Body)。
- **请求 Body**(对端校验必填 `request_date`,并与 PlayX 文档一致保留 `request_id``token`
| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `merchant_code` | String | 是 | 与 `PLAYX_ANGPOW_MERCHANT_CODE` 一致。 |
| `report_date` | String | 是 | 与参与签名的 `report_date` 一致(秒级时间戳字符串)。 |
| `request_id` | String | 是 | 商城生成的请求追踪号。 |
| `request_date` | String | 是 | Unix 秒时间戳字符串,须与参与签名的 `request_date` 一致。 |
| `token` | String | 是 | 前端传入的 playX 临时凭证。 |
请求示例:
```json
{
"merchant_code": "plx",
"report_date": "1700000000",
"request_id": "mall_20260319_9f1b6d",
"request_date": "1700000000",
"token": "eyJhbGciOi..."
}
```

View File

@@ -272,8 +272,8 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_
* 仅接受商城临时登录 token类型 **`muser`**),校验 `token` 表后写入 **`mall_session`**。
* 返回的 `data.user_id`**`playx_user_id`**(与资产表一致)。
* **`verify_token_local_only = false`**(生产对接 playX
* 需配置 **`playX.api.base_url`**,由商城向 playX 发起 `POST` 校验(请求/响应约定见 **`docs/playX-接口待完善清单.md`** 第一部分 §1)。
* 若未配置 `base_url`,返回 `playX API not configured`
* 需配置 **`PLAYX_TOKEN_VERIFY_URL`**(完整 `https` 回调或相对路径)及 **`PLAYX_ANGPOW_IMPORT_AUTH_KEY`** 等(见 `docs/PlayX-对接文档(积分商城).md` §5.2)。
* 相对路径时尚需 **`PLAYX_ANGPOW_IMPORT_BASE_URL`**;未配置则返回未配置类错误
#### 请求参数
@@ -281,6 +281,8 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_
* `token`Body 优先;`session` 兼容字段Query 也可传 `token`
可选 **`lang`**`zh` / `ZH` / `zh-cn` 中文 **`msg`**(默认),`en` / `EN` 英文可通过请求头、Query 或与 Body 同传的字段指定。
#### 返回(成功 data
| 字段 | 类型 | 说明 |

View File

@@ -297,12 +297,14 @@ curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \
### 5.1 `POST /api/v1/mall/verifyToken`
**用途**:把 playX token 换成商城 `session_id`
**语言 `lang`(响应 `msg` 等多语言)**
- **默认中文**`zh-cn`)。
- 可通过 **请求头 `lang`**、**Query `lang`** 或 **表单/JSON 字段 `lang`** 指定:`zh` / `ZH` / `zh-cn` → 中文,`en` / `EN` → 英文(与 `LoadLangPack` 规则一致)。
**前端入参约定(重要)**
- 前端/客户端调用本接口时,**只需要传 `token`**(或兼容 `session`)。
- 商城后端调用 playX 校验时:
- **`PLAYX_TOKEN_VERIFY_URL` 为完整 `https://` URL**(回调网关):**`X-Request-Signature` 生成方式与 angpow-imports`PLAYX_ANGPOW_IMPORT_PATH`)一致**`HMAC-SHA1``Base64`,密钥 `PLAYX_ANGPOW_IMPORT_AUTH_KEY`Body 含 **`merchant_code``report_date``request_id``token`**canonical 为 `merchant_code=...&report_date=...&request_id=...&token=...`
- 若为**相对路径**:后端会拼 **`PLAYX_ANGPOW_IMPORT_BASE_URL`**Body 使用 **`request_date`**,并生成 **`merchant_code` / `request_date` / `request_id` / `X-Request-Signature`** 等,前端仍不参与签名。
- 若仅传 `token` 仍校验失败,多为 token 无效/过期或上游 URL 未放通,并非前端少传字段。
- 调用本接口时 **仅需 `token`**(或兼容 `session`**不要**传 `request_id` / `request_date` / 商户字段(均由商城后端生成后请求上游)
- 建议 **`Content-Type: application/json`**Body`{"token":"..."}`。若使用 **form-data**(如图二),请同样只传 `token`(及可选 `lang`);避免缺少 `token` 键名。
- 商城请求上游回调地址时Body 为 **`request_id``request_date``token`**`X-Request-Signature` 与 angpow-imports 同源canonical 为 `merchant_code=...&request_date=...&request_id=...&token=...``merchant_code` 来自配置,仅用于签名)
**请求示例:**
```json
@@ -311,6 +313,15 @@ curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \
}
```
带英文响应示例Query
```http
POST /api/v1/mall/verifyToken?lang=en
Content-Type: application/json
{"token":"eyJhbGciOi..."}
```
**成功响应示例:**
```json
{
@@ -633,9 +644,9 @@ 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` | Angpow 推送等接口基地址;**相对路径** `verify-token` 时亦作其基地址 |
| `PLAYX_TOKEN_VERIFY_URL` | **完整 `https://` URL**:回调校验,签名与 angpow-imports 同源;**相对路径**:拼基地址 + `request_date` 商户体 |
| `PLAYX_ANGPOW_MERCHANT_CODE` | 回调与相对路径 `verify-token` 均需 |
| `PLAYX_ANGPOW_IMPORT_AUTH_KEY` | 回调与相对路径 `verify-token` 的 HMAC 密钥(与 angpow-imports 相同) |
| `PLAYX_TOKEN_VERIFY_URL` | **完整 `https://` URL**:回调 Body 为 `request_id`/`request_date`/`token`,签名 canonical 含配置项 `merchant_code`**相对路径**:拼基地址 + 商户四字段 |
| `PLAYX_ANGPOW_MERCHANT_CODE` | 回调仅写入签名字符串;相对路径同时进 Body |
| `PLAYX_ANGPOW_IMPORT_AUTH_KEY` | 回调与相对路径的 HMAC 密钥(与 angpow-imports 相同) |
---