fadd
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
# 查询白名单公开添加接口 — 对接文档
|
# 查询白名单公开接口 — 对接文档
|
||||||
|
|
||||||
> 面向**下游调用方**(API 用户 / 合作方),与上游数据源配置无关。
|
> 面向**下游调用方**(API 用户 / 合作方),与上游数据源配置无关。
|
||||||
> 平台侧仅在 `config.yaml` 顶部保留**一份** `query_whitelist.public_api` 配置。
|
> 平台侧仅在 `config.yaml` 顶部保留**一份** `query_whitelist.public_api` 配置。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 接口概述
|
## 1. 接口一览
|
||||||
|
|
||||||
| 项 | 说明 |
|
| 接口 | 方法 | 路径 | 用途 |
|
||||||
|----|------|
|
|------|------|------|------|
|
||||||
| 地址 | `POST /api/v1/query-whitelist/entries` |
|
| **创建规则** | POST | `/api/v1/query-whitelist/entries` | 新建一条屏蔽规则;同用户+身份证+姓名已存在则 **拒绝**(`1013`) |
|
||||||
| 用途 | 为**当前 access_id 对应用户**添加查询白名单规则 |
|
| **追加接口** | POST | `/api/v1/query-whitelist/entries/append` | 向**已有**规则追加 `api_codes`(去重合并),**不新建记录**;规则不存在则 **拒绝**(`1014`) |
|
||||||
| 生效范围 | **仅对该用户生效**(`user_id` 自动绑定,不可创建全局规则) |
|
|
||||||
| 计费 | 否 |
|
两条接口鉴权、加密、响应格式相同,仅业务语义不同。
|
||||||
|
|
||||||
命中白名单后,对应 API 调用将返回 `1000 查询为空`,不调用上游、不扣费。
|
命中白名单后,对应 API 调用将返回 `1000 查询为空`,不调用上游、不扣费。
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
## 2. 鉴权(双重 + IP)
|
## 2. 鉴权(双重 + IP)
|
||||||
|
|
||||||
### 2.1 请求头
|
### 2.1 请求头(两个接口共用)
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
| `Whitelist-Mgmt-Key` | string | 是 | 平台下发的**独立管理密钥**(与 Access Key 不同) |
|
| `Whitelist-Mgmt-Key` | string | 是 | 平台下发的**独立管理密钥**(与 Access Key 不同) |
|
||||||
| `Content-Type` | string | 是 | 固定为 `application/json` |
|
| `Content-Type` | string | 是 | 固定为 `application/json` |
|
||||||
|
|
||||||
### 2.2 请求体(加密)
|
### 2.2 请求体(加密,两个接口共用)
|
||||||
|
|
||||||
与业务 API 相同,外层仅传 `data` 字段:
|
与业务 API 相同,外层仅传 `data` 字段:
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
使用目标用户的 **Access Key**(16 进制 Secret Key)加密。Access Key **仅用于本地加密,不要写入明文 JSON**。
|
使用目标用户的 **Access Key**(16 进制 Secret Key)加密。Access Key **仅用于本地加密,不要写入明文 JSON**。
|
||||||
|
|
||||||
### 2.3 明文业务参数(加密前 JSON)
|
### 2.3 明文业务参数(加密前 JSON,两个接口字段相同)
|
||||||
|
|
||||||
**无需在明文里传 `key` 或 `access_id`**。身份校验方式与业务 API 相同:请求头携带 `Access-Id`,服务端用该用户 Access Key 解密 `data`,解密成功即证明调用方持有正确密钥。
|
**无需在明文里传 `key` 或 `access_id`**。身份校验方式与业务 API 相同:请求头携带 `Access-Id`,服务端用该用户 Access Key 解密 `data`,解密成功即证明调用方持有正确密钥。
|
||||||
|
|
||||||
@@ -61,7 +61,8 @@
|
|||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| `name` | string | 是 | 姓名;填 `*` 表示只匹配身份证,不校验姓名 |
|
| `name` | string | 是 | 姓名;填 `*` 表示只匹配身份证,不校验姓名 |
|
||||||
| `id_card` | string | 是 | 18 位身份证号 |
|
| `id_card` | string | 是 | 18 位身份证号 |
|
||||||
| `api_codes` | string[] | 是 | 生效的产品编码列表;**必须为 JSON 数组**,至少 1 个元素;元素类型为 string;**禁止**传字符串/对象等非数组类型;**禁止**包含通配符 `*`(不可写 `["*"]` 或与其他编码混用) |
|
| `api_codes` | string[] | 是 | 产品编码列表;**必须为 JSON 数组**,至少 1 个元素;元素为 string;**禁止**非数组类型;**禁止**通配符 `*` |
|
||||||
|
| `remark` | string | 否 | 备注,最长 500 字符;追加接口传入非空时会覆盖原备注 |
|
||||||
|
|
||||||
> **`api_codes` 约束(公开接口专属)**
|
> **`api_codes` 约束(公开接口专属)**
|
||||||
>
|
>
|
||||||
@@ -72,7 +73,6 @@
|
|||||||
> | 通配全部 `*` | ❌ | `["*"]` |
|
> | 通配全部 `*` | ❌ | `["*"]` |
|
||||||
> | 空数组 | ❌ | `[]` |
|
> | 空数组 | ❌ | `[]` |
|
||||||
> | 非字符串元素 | ❌ | `[123]`、`[{}]` |
|
> | 非字符串元素 | ❌ | `[123]`、`[{}]` |
|
||||||
| `remark` | string | 否 | 备注,最长 500 字符 |
|
|
||||||
|
|
||||||
### 2.4 IP 白名单
|
### 2.4 IP 白名单
|
||||||
|
|
||||||
@@ -80,7 +80,61 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 加密算法
|
## 3. 两个接口的业务区别
|
||||||
|
|
||||||
|
规则的唯一键为 **`user_id` + 身份证 + `name`**,每个组合在表里只有**一条**记录,`api_codes` 存在该行的 JSON 数组中。
|
||||||
|
|
||||||
|
### 3.1 创建接口 `POST /entries`
|
||||||
|
|
||||||
|
用于**首次**为某身份证建立屏蔽规则。
|
||||||
|
|
||||||
|
| 场景 | 行为 |
|
||||||
|
|------|------|
|
||||||
|
| 规则不存在 | 新建 1 条记录 |
|
||||||
|
| 同用户+身份证+姓名已存在 | 返回 `1013 规则已存在`,**不新建、不合并** |
|
||||||
|
|
||||||
|
示例:第一次传 `api_codes: ["FLXG0V4B"]` → 成功新建。
|
||||||
|
|
||||||
|
### 3.2 追加接口 `POST /entries/append`
|
||||||
|
|
||||||
|
用于在**已有规则**上补充更多生效接口,适合「先挡一个、后续再加」的场景。
|
||||||
|
|
||||||
|
| 场景 | 行为 |
|
||||||
|
|------|------|
|
||||||
|
| 规则已存在 | **更新原记录**:将本次 `api_codes` 与已有列表**去重合并**(保留原顺序,新编码追加在后),**不新建第二条** |
|
||||||
|
| 本次编码均已存在 | 仍返回成功(幂等),`api_codes` 不变 |
|
||||||
|
| 规则不存在 | 返回 `1014 规则不存在,请先调用创建接口` |
|
||||||
|
| 原规则 `api_codes` 含 `*`(全部接口) | 返回 `1003`,公开接口不支持对「全部接口」规则追加 |
|
||||||
|
|
||||||
|
**追加示例:**
|
||||||
|
|
||||||
|
```
|
||||||
|
已有记录:user_id=U1, id_card=350681..., name=*, api_codes=["FLXG0V4B"]
|
||||||
|
|
||||||
|
第二次调用 append:
|
||||||
|
api_codes: ["JRZQ8A2D"]
|
||||||
|
|
||||||
|
结果(同一条记录被更新):
|
||||||
|
api_codes: ["FLXG0V4B", "JRZQ8A2D"] ← 合并去重,非新建
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
第三次再 append:
|
||||||
|
api_codes: ["JRZQ8A2D", "FLXG2E8F"]
|
||||||
|
|
||||||
|
结果:
|
||||||
|
api_codes: ["FLXG0V4B", "JRZQ8A2D", "FLXG2E8F"] ← JRZQ8A2D 已存在则跳过
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 推荐调用顺序
|
||||||
|
|
||||||
|
1. 首次屏蔽 → 调 **创建接口**,可一次传齐所有 `api_codes`
|
||||||
|
2. 后续补接口 → 调 **追加接口**,只传**新增**的编码即可
|
||||||
|
3. 若创建时返回 `1013` → 改调 **追加接口**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 加密算法
|
||||||
|
|
||||||
与业务 API 完全一致(AES-128-CBC + PKCS7 + 随机 IV):
|
与业务 API 完全一致(AES-128-CBC + PKCS7 + 随机 IV):
|
||||||
|
|
||||||
@@ -92,7 +146,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 响应格式
|
## 5. 响应格式
|
||||||
|
|
||||||
与业务 API 一致:
|
与业务 API 一致:
|
||||||
|
|
||||||
@@ -127,11 +181,11 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 错误码
|
## 6. 错误码
|
||||||
|
|
||||||
| code | message | 说明 |
|
| code | message | 说明 |
|
||||||
|------|---------|------|
|
|------|---------|------|
|
||||||
| 0 | 业务成功 | 创建成功 |
|
| 0 | 业务成功 | 创建或追加成功 |
|
||||||
| 1002 | 解密失败 | `data` 解密失败 |
|
| 1002 | 解密失败 | `data` 解密失败 |
|
||||||
| 1003 | 请求参数结构不正确 | 明文参数缺失或格式错误 |
|
| 1003 | 请求参数结构不正确 | 明文参数缺失或格式错误 |
|
||||||
| 1004 | 未经授权的IP | IP 不在白名单 |
|
| 1004 | 未经授权的IP | IP 不在白名单 |
|
||||||
@@ -140,12 +194,15 @@
|
|||||||
| 1010 | 缺少管理密钥 | 未传 `Whitelist-Mgmt-Key` |
|
| 1010 | 缺少管理密钥 | 未传 `Whitelist-Mgmt-Key` |
|
||||||
| 1011 | 管理密钥无效 | 管理密钥错误 |
|
| 1011 | 管理密钥无效 | 管理密钥错误 |
|
||||||
| 1012 | 公开接口未启用 | 服务端 `public_api.enabled=false` |
|
| 1012 | 公开接口未启用 | 服务端 `public_api.enabled=false` |
|
||||||
| 1013 | 规则已存在 | 同用户下身份证+姓名重复 |
|
| 1013 | 规则已存在 | 创建接口:同用户下身份证+姓名重复 |
|
||||||
|
| 1014 | 规则不存在 | 追加接口:须先调用创建接口 |
|
||||||
| 1001 | 接口异常 | 系统错误 |
|
| 1001 | 接口异常 | 系统错误 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Node.js 调用示例
|
## 7. Node.js 调用示例
|
||||||
|
|
||||||
|
### 7.1 创建规则
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@@ -153,7 +210,7 @@ const crypto = require('crypto');
|
|||||||
const BASE_URL = 'https://api.tianyuanapi.com';
|
const BASE_URL = 'https://api.tianyuanapi.com';
|
||||||
const ACCESS_ID = process.env.ACCESS_ID;
|
const ACCESS_ID = process.env.ACCESS_ID;
|
||||||
const ACCESS_KEY = process.env.ACCESS_KEY; // 用户 Access Key
|
const ACCESS_KEY = process.env.ACCESS_KEY; // 用户 Access Key
|
||||||
const MGMT_KEY = '2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL'; // 平台管理密钥,见文档 §7
|
const MGMT_KEY = '2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL'; // 平台管理密钥,见文档 §8
|
||||||
|
|
||||||
function encrypt(data, keyHex) {
|
function encrypt(data, keyHex) {
|
||||||
const key = Buffer.from(keyHex, 'hex');
|
const key = Buffer.from(keyHex, 'hex');
|
||||||
@@ -204,9 +261,40 @@ async function addWhitelistEntry() {
|
|||||||
addWhitelistEntry();
|
addWhitelistEntry();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 7.2 追加生效接口(去重合并)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function appendWhitelistApiCodes() {
|
||||||
|
const payload = {
|
||||||
|
name: '*',
|
||||||
|
id_card: '350681198611130611',
|
||||||
|
api_codes: ['JRZQ8A2D'], // 只传本次要追加的编码
|
||||||
|
remark: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${BASE_URL}/api/v1/query-whitelist/entries/append`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Id': ACCESS_ID,
|
||||||
|
'Whitelist-Mgmt-Key': MGMT_KEY,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ data: encrypt(JSON.stringify(payload), ACCESS_KEY) }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
console.log('追加响应:', json);
|
||||||
|
if (json.code === 0 && json.data) {
|
||||||
|
console.log('合并后规则:', JSON.parse(decrypt(json.data, ACCESS_KEY)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendWhitelistApiCodes();
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 平台配置(仅一份)
|
## 8. 平台配置(仅一份)
|
||||||
|
|
||||||
`config.yaml` 顶部(`app` 段之后),**不要**写在上游数据源配置块内:
|
`config.yaml` 顶部(`app` 段之后),**不要**写在上游数据源配置块内:
|
||||||
|
|
||||||
@@ -232,7 +320,7 @@ query_whitelist:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 字段说明补充
|
## 9. 字段说明补充
|
||||||
|
|
||||||
### `name = *`
|
### `name = *`
|
||||||
|
|
||||||
@@ -248,7 +336,7 @@ query_whitelist:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 数据库变更
|
## 10. 数据库变更
|
||||||
|
|
||||||
新增字段 `operation_ip`(记录公开接口添加时的客户端 IP):
|
新增字段 `operation_ip`(记录公开接口添加时的客户端 IP):
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ var (
|
|||||||
ErrInvalidMgmtKey = errors.New("管理密钥无效")
|
ErrInvalidMgmtKey = errors.New("管理密钥无效")
|
||||||
ErrPublicAPIDisabled = errors.New("公开接口未启用")
|
ErrPublicAPIDisabled = errors.New("公开接口未启用")
|
||||||
ErrWhitelistExists = errors.New("规则已存在")
|
ErrWhitelistExists = errors.New("规则已存在")
|
||||||
|
ErrWhitelistNotFound = errors.New("规则不存在")
|
||||||
)
|
)
|
||||||
|
|
||||||
// 错误码映射 - 严格按照用户要求
|
// 错误码映射 - 严格按照用户要求
|
||||||
@@ -58,6 +59,7 @@ var ErrorCodeMap = map[error]int{
|
|||||||
ErrInvalidMgmtKey: 1011,
|
ErrInvalidMgmtKey: 1011,
|
||||||
ErrPublicAPIDisabled: 1012,
|
ErrPublicAPIDisabled: 1012,
|
||||||
ErrWhitelistExists: 1013,
|
ErrWhitelistExists: 1013,
|
||||||
|
ErrWhitelistNotFound: 1014,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetErrorCode 获取错误对应的错误码
|
// GetErrorCode 获取错误对应的错误码
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
type QueryWhitelistApplicationService interface {
|
type QueryWhitelistApplicationService interface {
|
||||||
CreateEntry(ctx context.Context, adminUserID string, req *dto.QueryWhitelistEntryRequest) (*dto.QueryWhitelistEntryResponse, error)
|
CreateEntry(ctx context.Context, adminUserID string, req *dto.QueryWhitelistEntryRequest) (*dto.QueryWhitelistEntryResponse, error)
|
||||||
CreateEntryPublic(ctx context.Context, headerAccessID, managementKey, clientIP, encryptedData string) (*dto.QueryWhitelistEntryResponse, string, error)
|
CreateEntryPublic(ctx context.Context, headerAccessID, managementKey, clientIP, encryptedData string) (*dto.QueryWhitelistEntryResponse, string, error)
|
||||||
|
AppendEntryPublic(ctx context.Context, headerAccessID, managementKey, clientIP, encryptedData string) (*dto.QueryWhitelistEntryResponse, string, error)
|
||||||
UpdateEntry(ctx context.Context, adminUserID, id string, req *dto.QueryWhitelistEntryUpdateRequest) (*dto.QueryWhitelistEntryResponse, error)
|
UpdateEntry(ctx context.Context, adminUserID, id string, req *dto.QueryWhitelistEntryUpdateRequest) (*dto.QueryWhitelistEntryResponse, error)
|
||||||
UpdateEntryStatus(ctx context.Context, adminUserID, id, status string) (*dto.QueryWhitelistEntryResponse, error)
|
UpdateEntryStatus(ctx context.Context, adminUserID, id, status string) (*dto.QueryWhitelistEntryResponse, error)
|
||||||
DeleteEntry(ctx context.Context, id string) error
|
DeleteEntry(ctx context.Context, id string) error
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"tyapi-server/internal/application/api/dto"
|
"tyapi-server/internal/application/api/dto"
|
||||||
@@ -11,9 +13,9 @@ import (
|
|||||||
"tyapi-server/internal/domains/api/entities"
|
"tyapi-server/internal/domains/api/entities"
|
||||||
api_services "tyapi-server/internal/domains/api/services"
|
api_services "tyapi-server/internal/domains/api/services"
|
||||||
"tyapi-server/internal/shared/crypto"
|
"tyapi-server/internal/shared/crypto"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const queryWhitelistMgmtKeyHeader = "Whitelist-Mgmt-Key"
|
const queryWhitelistMgmtKeyHeader = "Whitelist-Mgmt-Key"
|
||||||
@@ -23,61 +25,17 @@ func QueryWhitelistMgmtKeyHeader() string {
|
|||||||
return queryWhitelistMgmtKeyHeader
|
return queryWhitelistMgmtKeyHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateEntryPublic 公开接口:解密业务参数后为当前 access_id 对应用户添加规则(仅对该用户生效)
|
// CreateEntryPublic 公开接口:新建规则(同用户+身份证+姓名已存在则拒绝)
|
||||||
func (s *QueryWhitelistApplicationServiceImpl) CreateEntryPublic(
|
func (s *QueryWhitelistApplicationServiceImpl) CreateEntryPublic(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
headerAccessID, managementKey, clientIP, encryptedData string,
|
headerAccessID, managementKey, clientIP, encryptedData string,
|
||||||
) (*dto.QueryWhitelistEntryResponse, string, error) {
|
) (*dto.QueryWhitelistEntryResponse, string, error) {
|
||||||
if !s.config.QueryWhitelist.PublicAPI.Enabled {
|
apiUser, payload, err := s.preparePublicRequest(ctx, headerAccessID, managementKey, clientIP, encryptedData)
|
||||||
return nil, "", ErrPublicAPIDisabled
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(managementKey) == "" {
|
|
||||||
return nil, "", ErrMissingMgmtKey
|
|
||||||
}
|
|
||||||
if !constantTimeEqual(managementKey, s.config.QueryWhitelist.PublicAPI.ManagementKey) {
|
|
||||||
return nil, "", ErrInvalidMgmtKey
|
|
||||||
}
|
|
||||||
headerAccessID = strings.TrimSpace(headerAccessID)
|
|
||||||
if headerAccessID == "" {
|
|
||||||
return nil, "", ErrMissingAccessId
|
|
||||||
}
|
|
||||||
|
|
||||||
apiUser, err := s.apiUserService.LoadApiUserByAccessId(ctx, headerAccessID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("公开白名单接口 AccessId 无效", zap.String("access_id", headerAccessID), zap.Error(err))
|
return nil, "", err
|
||||||
return nil, "", ErrInvalidAccessId
|
|
||||||
}
|
|
||||||
if apiUser.IsFrozen() {
|
|
||||||
return nil, "", ErrFrozenAccount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypted, err := crypto.AesDecrypt(encryptedData, apiUser.SecretKey)
|
createdBy := "public_api:" + strings.TrimSpace(headerAccessID)
|
||||||
if err != nil {
|
|
||||||
s.logger.Warn("公开白名单接口解密失败", zap.String("access_id", headerAccessID), zap.Error(err))
|
|
||||||
return nil, "", ErrDecryptFail
|
|
||||||
}
|
|
||||||
|
|
||||||
var raw map[string]json.RawMessage
|
|
||||||
if err := json.Unmarshal(decrypted, &raw); err != nil {
|
|
||||||
return nil, "", ErrRequestParam
|
|
||||||
}
|
|
||||||
if err := validatePublicAPICodesField(raw["api_codes"]); err != nil {
|
|
||||||
return nil, "", MapWhitelistAppError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload dto.QueryWhitelistPublicPayload
|
|
||||||
if err := json.Unmarshal(decrypted, &payload); err != nil {
|
|
||||||
return nil, "", ErrRequestParam
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.config.App.IsDevelopment() && !apiUser.IsWhiteListed(clientIP) {
|
|
||||||
s.logger.Warn("公开白名单接口 IP 未授权",
|
|
||||||
zap.String("access_id", headerAccessID),
|
|
||||||
zap.String("client_ip", clientIP))
|
|
||||||
return nil, "", ErrInvalidIP
|
|
||||||
}
|
|
||||||
|
|
||||||
createdBy := "public_api:" + headerAccessID
|
|
||||||
resp, err := s.createEntry(ctx, createdBy, &dto.QueryWhitelistEntryRequest{
|
resp, err := s.createEntry(ctx, createdBy, &dto.QueryWhitelistEntryRequest{
|
||||||
UserID: apiUser.UserId,
|
UserID: apiUser.UserId,
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
@@ -88,6 +46,115 @@ func (s *QueryWhitelistApplicationServiceImpl) CreateEntryPublic(
|
|||||||
return resp, apiUser.SecretKey, MapWhitelistAppError(err)
|
return resp, apiUser.SecretKey, MapWhitelistAppError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppendEntryPublic 公开接口:向已有规则追加 api_codes(去重合并,不新建记录)
|
||||||
|
func (s *QueryWhitelistApplicationServiceImpl) AppendEntryPublic(
|
||||||
|
ctx context.Context,
|
||||||
|
headerAccessID, managementKey, clientIP, encryptedData string,
|
||||||
|
) (*dto.QueryWhitelistEntryResponse, string, error) {
|
||||||
|
apiUser, payload, err := s.preparePublicRequest(ctx, headerAccessID, managementKey, clientIP, encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateIDCard(payload.IDCard); err != nil {
|
||||||
|
return nil, "", MapWhitelistAppError(err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(payload.Name) == "" {
|
||||||
|
return nil, "", ErrRequestParam
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := apiUser.UserId
|
||||||
|
idCardHash := api_services.HashIDCard(payload.IDCard)
|
||||||
|
name := normalizeWhitelistName(payload.Name)
|
||||||
|
|
||||||
|
entry, err := s.repo.FindByUserIDCardHashAndName(ctx, userID, idCardHash, name)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, "", ErrWhitelistNotFound
|
||||||
|
}
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, code := range entry.APICodes {
|
||||||
|
if code == "*" {
|
||||||
|
return nil, "", ErrRequestParam
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := mergeAPICodes([]string(entry.APICodes), payload.APICodes)
|
||||||
|
entry.APICodes = entities.APICodeList(merged)
|
||||||
|
if remark := strings.TrimSpace(payload.Remark); remark != "" {
|
||||||
|
entry.Remark = remark
|
||||||
|
}
|
||||||
|
updatedBy := "public_api_append:" + strings.TrimSpace(headerAccessID)
|
||||||
|
entry.UpdatedBy = &updatedBy
|
||||||
|
entry.OperationIP = strings.TrimSpace(clientIP)
|
||||||
|
|
||||||
|
if err := s.repo.Update(ctx, entry); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
s.queryWhitelistSvc.InvalidateCache(entry.UserID, idCardHash)
|
||||||
|
|
||||||
|
resp := dto.NewQueryWhitelistEntryResponse(entry)
|
||||||
|
return &resp, apiUser.SecretKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *QueryWhitelistApplicationServiceImpl) preparePublicRequest(
|
||||||
|
ctx context.Context,
|
||||||
|
headerAccessID, managementKey, clientIP, encryptedData string,
|
||||||
|
) (*entities.ApiUser, dto.QueryWhitelistPublicPayload, error) {
|
||||||
|
if !s.config.QueryWhitelist.PublicAPI.Enabled {
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrPublicAPIDisabled
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(managementKey) == "" {
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrMissingMgmtKey
|
||||||
|
}
|
||||||
|
if !constantTimeEqual(managementKey, s.config.QueryWhitelist.PublicAPI.ManagementKey) {
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrInvalidMgmtKey
|
||||||
|
}
|
||||||
|
headerAccessID = strings.TrimSpace(headerAccessID)
|
||||||
|
if headerAccessID == "" {
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrMissingAccessId
|
||||||
|
}
|
||||||
|
|
||||||
|
apiUser, err := s.apiUserService.LoadApiUserByAccessId(ctx, headerAccessID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("公开白名单接口 AccessId 无效", zap.String("access_id", headerAccessID), zap.Error(err))
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrInvalidAccessId
|
||||||
|
}
|
||||||
|
if apiUser.IsFrozen() {
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrFrozenAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := crypto.AesDecrypt(encryptedData, apiUser.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("公开白名单接口解密失败", zap.String("access_id", headerAccessID), zap.Error(err))
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrDecryptFail
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(decrypted, &raw); err != nil {
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrRequestParam
|
||||||
|
}
|
||||||
|
if err := validatePublicAPICodesField(raw["api_codes"]); err != nil {
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, MapWhitelistAppError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload dto.QueryWhitelistPublicPayload
|
||||||
|
if err := json.Unmarshal(decrypted, &payload); err != nil {
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrRequestParam
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.config.App.IsDevelopment() && !apiUser.IsWhiteListed(clientIP) {
|
||||||
|
s.logger.Warn("公开白名单接口 IP 未授权",
|
||||||
|
zap.String("access_id", headerAccessID),
|
||||||
|
zap.String("client_ip", clientIP))
|
||||||
|
return nil, dto.QueryWhitelistPublicPayload{}, ErrInvalidIP
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiUser, payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *QueryWhitelistApplicationServiceImpl) createEntry(
|
func (s *QueryWhitelistApplicationServiceImpl) createEntry(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
createdBy string,
|
createdBy string,
|
||||||
@@ -126,6 +193,35 @@ func (s *QueryWhitelistApplicationServiceImpl) createEntry(
|
|||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergeAPICodes 合并接口编码并去重,保持原有顺序,新增编码追加在后
|
||||||
|
func mergeAPICodes(existing, incoming []string) []string {
|
||||||
|
seen := make(map[string]struct{}, len(existing)+len(incoming))
|
||||||
|
result := make([]string, 0, len(existing)+len(incoming))
|
||||||
|
for _, code := range existing {
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[code]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[code] = struct{}{}
|
||||||
|
result = append(result, code)
|
||||||
|
}
|
||||||
|
for _, code := range incoming {
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[code]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[code] = struct{}{}
|
||||||
|
result = append(result, code)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func constantTimeEqual(a, b string) bool {
|
func constantTimeEqual(a, b string) bool {
|
||||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||||
}
|
}
|
||||||
@@ -205,6 +301,8 @@ func PublicAPIErrorMessage(err error) string {
|
|||||||
switch err {
|
switch err {
|
||||||
case ErrWhitelistExists:
|
case ErrWhitelistExists:
|
||||||
return "规则已存在"
|
return "规则已存在"
|
||||||
|
case ErrWhitelistNotFound:
|
||||||
|
return "规则不存在,请先调用创建接口"
|
||||||
case ErrRequestParam:
|
case ErrRequestParam:
|
||||||
return "请求参数结构不正确"
|
return "请求参数结构不正确"
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -32,3 +32,16 @@ func TestValidatePublicAPICodesField(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeAPICodes(t *testing.T) {
|
||||||
|
got := mergeAPICodes([]string{"FLXG0V4B", "JRZQ8A2D"}, []string{"JRZQ8A2D", "FLXG2E8F"})
|
||||||
|
want := []string{"FLXG0V4B", "JRZQ8A2D", "FLXG2E8F"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("mergeAPICodes() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Fatalf("mergeAPICodes() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ type QueryWhitelistRepository interface {
|
|||||||
FindAllEnabled(ctx context.Context) ([]*entities.QueryWhitelistEntry, error)
|
FindAllEnabled(ctx context.Context) ([]*entities.QueryWhitelistEntry, error)
|
||||||
List(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.QueryWhitelistEntry, int64, error)
|
List(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.QueryWhitelistEntry, int64, error)
|
||||||
ExistsByUserIDCardHashAndName(ctx context.Context, userID, idCardHash, name string, excludeID string) (bool, error)
|
ExistsByUserIDCardHashAndName(ctx context.Context, userID, idCardHash, name string, excludeID string) (bool, error)
|
||||||
|
FindByUserIDCardHashAndName(ctx context.Context, userID, idCardHash, name string) (*entities.QueryWhitelistEntry, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ func (m *mockQueryWhitelistRepo) List(ctx context.Context, filters map[string]in
|
|||||||
func (m *mockQueryWhitelistRepo) ExistsByUserIDCardHashAndName(ctx context.Context, userID, idCardHash, name, excludeID string) (bool, error) {
|
func (m *mockQueryWhitelistRepo) ExistsByUserIDCardHashAndName(ctx context.Context, userID, idCardHash, name, excludeID string) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
func (m *mockQueryWhitelistRepo) FindByUserIDCardHashAndName(ctx context.Context, userID, idCardHash, name string) (*entities.QueryWhitelistEntry, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockQueryWhitelistRepo) FindEnabledByUserIDsAndIDCardHash(ctx context.Context, userIDs []string, idCardHash string) ([]*entities.QueryWhitelistEntry, error) {
|
func (m *mockQueryWhitelistRepo) FindEnabledByUserIDsAndIDCardHash(ctx context.Context, userIDs []string, idCardHash string) ([]*entities.QueryWhitelistEntry, error) {
|
||||||
var result []*entities.QueryWhitelistEntry
|
var result []*entities.QueryWhitelistEntry
|
||||||
|
|||||||
@@ -150,3 +150,17 @@ func (r *GormQueryWhitelistRepository) ExistsByUserIDCardHashAndName(
|
|||||||
}
|
}
|
||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *GormQueryWhitelistRepository) FindByUserIDCardHashAndName(
|
||||||
|
ctx context.Context,
|
||||||
|
userID, idCardHash, name string,
|
||||||
|
) (*entities.QueryWhitelistEntry, error) {
|
||||||
|
var entry entities.QueryWhitelistEntry
|
||||||
|
err := r.GetDB(ctx).
|
||||||
|
Where("user_id = ? AND id_card_hash = ? AND name = ?", userID, idCardHash, name).
|
||||||
|
First(&entry).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &entry, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PublicQueryWhitelistHandler 查询白名单公开添加接口(面向下游调用方)
|
// PublicQueryWhitelistHandler 查询白名单公开接口(面向下游调用方)
|
||||||
type PublicQueryWhitelistHandler struct {
|
type PublicQueryWhitelistHandler struct {
|
||||||
appService api_app.QueryWhitelistApplicationService
|
appService api_app.QueryWhitelistApplicationService
|
||||||
validator interfaces.RequestValidator
|
validator interfaces.RequestValidator
|
||||||
@@ -34,6 +34,21 @@ func NewPublicQueryWhitelistHandler(
|
|||||||
|
|
||||||
// CreateEntry 公开添加查询白名单规则
|
// CreateEntry 公开添加查询白名单规则
|
||||||
func (h *PublicQueryWhitelistHandler) CreateEntry(c *gin.Context) {
|
func (h *PublicQueryWhitelistHandler) CreateEntry(c *gin.Context) {
|
||||||
|
h.handlePublicEntry(c, func(ctx *gin.Context, accessID, mgmtKey, clientIP, data string) (any, string, error) {
|
||||||
|
return h.appService.CreateEntryPublic(ctx.Request.Context(), accessID, mgmtKey, clientIP, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendEntry 向已有规则追加生效接口(去重合并)
|
||||||
|
func (h *PublicQueryWhitelistHandler) AppendEntry(c *gin.Context) {
|
||||||
|
h.handlePublicEntry(c, func(ctx *gin.Context, accessID, mgmtKey, clientIP, data string) (any, string, error) {
|
||||||
|
return h.appService.AppendEntryPublic(ctx.Request.Context(), accessID, mgmtKey, clientIP, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicEntryHandler func(*gin.Context, string, string, string, string) (any, string, error)
|
||||||
|
|
||||||
|
func (h *PublicQueryWhitelistHandler) handlePublicEntry(c *gin.Context, fn publicEntryHandler) {
|
||||||
accessID := c.GetHeader("Access-Id")
|
accessID := c.GetHeader("Access-Id")
|
||||||
if accessID == "" {
|
if accessID == "" {
|
||||||
c.JSON(200, dto.NewErrorResponse(1005, "缺少Access-Id", ""))
|
c.JSON(200, dto.NewErrorResponse(1005, "缺少Access-Id", ""))
|
||||||
@@ -53,13 +68,7 @@ func (h *PublicQueryWhitelistHandler) CreateEntry(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transactionID := uuid.New().String()
|
transactionID := uuid.New().String()
|
||||||
result, secretKey, err := h.appService.CreateEntryPublic(
|
result, secretKey, err := fn(c, accessID, mgmtKey, c.ClientIP(), req.Data)
|
||||||
c.Request.Context(),
|
|
||||||
accessID,
|
|
||||||
mgmtKey,
|
|
||||||
c.ClientIP(),
|
|
||||||
req.Data,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
code := api_app.GetErrorCode(err)
|
code := api_app.GetErrorCode(err)
|
||||||
message := api_app.PublicAPIErrorMessage(err)
|
message := api_app.PublicAPIErrorMessage(err)
|
||||||
|
|||||||
@@ -31,5 +31,6 @@ func (r *PublicQueryWhitelistRoutes) Register(router *sharedhttp.GinRouter) {
|
|||||||
group := router.GetEngine().Group("/api/v1/query-whitelist")
|
group := router.GetEngine().Group("/api/v1/query-whitelist")
|
||||||
group.Use(r.domainAuthMiddleware.Handle(""))
|
group.Use(r.domainAuthMiddleware.Handle(""))
|
||||||
group.POST("/entries", r.handler.CreateEntry)
|
group.POST("/entries", r.handler.CreateEntry)
|
||||||
|
group.POST("/entries/append", r.handler.AppendEntry)
|
||||||
r.logger.Info("查询白名单公开接口路由注册完成")
|
r.logger.Info("查询白名单公开接口路由注册完成")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user