Files
qhmes/.trae/skills/jimureport/references/signature.md

212 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 接口签名机制 (JimuSignature)
## 概述
积木报表部分接口使用 `@JimuSignature` 注解标记,调用时需要在请求 Header 中携带签名参数,否则返回 `code: 1001` 签名校验失败错误。
**源码位置:**
- 后端拦截器:`jimureport-spring-boot-starter/.../common/interceptor/JimuReportSignatureInterceptor.java`
- 前端签名工具:`static/jmreport/desreport_/js/biz/SignMd5Util.js`
- 前端请求拦截器:`static/jmreport/desreport_/js/core/request.js`
## 需要签名的接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/jmreport/queryFieldBySql` | POST | SQL 解析获取字段 |
| `/jmreport/executeSelectApi` | POST | API 数据集解析 |
| `/jmreport/loadTableData` | POST | 加载表数据 |
| `/jmreport/testConnection` | POST | 测试数据源连接 |
| `/jmreport/download/image` | GET | 下载图片 |
| `/jmreport/dictCodeSearch` | GET | 字典编码搜索 |
| `/jmreport/getDataSourceByPage` | GET | 分页查询数据源 |
| `/jmreport/getDataSourceById` | GET | 按ID查询数据源 |
**不需要签名的接口(常用):**
- `/jmreport/save` — 保存报表
- `/jmreport/saveDb` — 保存数据集
- `/jmreport/get/{id}` — 获取报表
- `/jmreport/field/tree/{reportId}` — 获取数据集树
- `/jmreport/loadDbData/{dbId}` — 加载数据集详情
## 签名算法
### 请求 Headers
| Header | 值 | 说明 |
|--------|------|------|
| `X-Sign` | MD5 签名(大写) | 见下方计算方法 |
| `X-TIMESTAMP` | 当前时间戳(毫秒) | `int(time.time() * 1000)` |
### 计算步骤
```
1. 收集所有请求参数URL query参数 + POST body参数
2. 按 key 字母升序排序SortedMap / TreeMap
3. 转为 JSON 字符串JSON.stringify(sortedParams) 或 JSONObject.toJSONString(sortedMap)
4. 拼接签名密钥jsonStr + signatureSecret
5. 计算 MD5 并转大写MD5(jsonStr + secret).toUpperCase()
```
### 签名密钥 (signatureSecret)
**默认值:** `dd05f1c54d63749eda95f9fa6d49v442a`
解析优先级:
1. `JmReportBaseConfig.getSignatureSecret()` — 代码配置
2. Spring 属性 `jeecg.signatureSecret` — application.yml 配置
3. 默认值 `dd05f1c54d63749eda95f9fa6d49v442a`
> **注意:** 默认密钥中第29个字符是字母 `v`,不是数字 `4`。
### 时间戳校验
服务端校验时间戳有效期为 **5 分钟300秒**。如果客户端与服务器时间差超过5分钟会返回 "签名验证失败:X-TIMESTAMP已过期"。
### 参数值类型转换规则
前端在签名前会统一类型(后端用 `json.getString(key)` 读取,也是字符串):
- **数字** → 转为字符串(如 `0``"0"`
- **布尔** → 转为字符串(如 `false``"false"`
- **对象/数组** → 转为 JSON 字符串
- **null/空** → 不参与签名
### 后端校验逻辑
```java
// 1. 收集参数到 SortedMap (TreeMap, 自动按key排序)
SortedMap<String, String> map = new TreeMap<>();
// 2. 从 request.getParameterMap() 取 URL/form 参数
// 3. 从 request.getQueryString() 取 GET 参数
// 4. 从 POST body JSON 取参数 (json.getString(key))
// 5. 计算签名
String paramsJsonStr = JSONObject.toJSONString(map); // fastjson
String signValue = DigestUtils.md5DigestAsHex(
(paramsJsonStr + CommonUtils.getSignatureSecret()).getBytes()
).toUpperCase();
// 6. 比对 header 中的 X-Sign
```
> **关键细节:** 后端用 fastjson 的 `JSONObject.toJSONString(map)` 序列化 SortedMap输出格式为 `{"key1":"value1","key2":"value2"}`无空格。Python 端必须用 `json.dumps(sorted_dict, separators=(',', ':'))` 匹配(无空格)。
## Python 实现
```python
import hashlib
import json
import time
SIGNATURE_SECRET = "dd05f1c54d63749eda95f9fa6d49v442a"
def compute_sign(params_dict):
"""
计算积木报表接口签名
params_dict: 请求参数字典POST body 或 GET query 参数)
"""
# 1. 所有值转为字符串(与前端/后端保持一致)
str_params = {}
for k, v in params_dict.items():
if v is None:
continue
if isinstance(v, bool):
str_params[k] = str(v).lower() # True -> "true"
elif isinstance(v, (int, float)):
str_params[k] = str(v)
elif isinstance(v, (dict, list)):
str_params[k] = json.dumps(v, ensure_ascii=False, separators=(',', ':'))
else:
str_params[k] = str(v)
# 2. 按 key 字母升序排序
sorted_params = dict(sorted(str_params.items()))
# 3. 转为 JSON 字符串(无空格,与 fastjson 一致)
params_json = json.dumps(sorted_params, ensure_ascii=False, separators=(',', ':'))
# 4. 拼接密钥并计算 MD5
sign_str = params_json + SIGNATURE_SECRET
sign_value = hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
return sign_value
def get_sign_headers(params_dict):
"""获取签名相关的请求头"""
return {
'X-Sign': compute_sign(params_dict),
'X-TIMESTAMP': str(int(time.time() * 1000))
}
```
### 使用示例
```python
# POST /jmreport/queryFieldBySql
body = {"sql": "select * from demo", "dbSource": "", "type": "0"}
sign_headers = get_sign_headers(body)
# sign_headers = {'X-Sign': 'AB12CD34...', 'X-TIMESTAMP': '1774019281912'}
# GET /jmreport/getDataSourceByPage?pageNo=1&pageSize=10
query_params = {"pageNo": "1", "pageSize": "10"}
sign_headers = get_sign_headers(query_params)
```
### 完整的带签名 API 请求函数
```python
def api_request(path, data=None, method=None):
"""发送 API 请求,自动判断是否需要签名"""
url = f'{API_BASE}{path}'
headers = {
'X-Access-Token': TOKEN,
'Content-Type': 'application/json; charset=UTF-8'
}
# 需要签名的接口列表
SIGNED_ENDPOINTS = [
'/jmreport/queryFieldBySql',
'/jmreport/executeSelectApi',
'/jmreport/loadTableData',
'/jmreport/testConnection',
'/jmreport/download/image',
'/jmreport/dictCodeSearch',
'/jmreport/getDataSourceByPage',
'/jmreport/getDataSourceById',
]
# 判断是否需要签名
need_sign = any(path.rstrip('/').endswith(ep.rstrip('/')) for ep in SIGNED_ENDPOINTS)
if need_sign:
sign_params = data if data else {}
headers['X-TIMESTAMP'] = str(int(time.time() * 1000))
headers['X-Sign'] = compute_sign(sign_params)
if data is not None:
json_data = json.dumps(data, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(url, data=json_data, headers=headers, method=method or 'POST')
else:
req = urllib.request.Request(url, headers=headers, method=method or 'GET')
resp = urllib.request.urlopen(req, context=ctx)
return json.loads(resp.read().decode('utf-8'))
```
## 常见错误
| 错误信息 | 原因 | 解决方案 |
|---------|------|---------|
| `签名验证失败: 签名参数不存在` | 未传 X-Sign 或 X-TIMESTAMP Header | 添加签名 Headers |
| `签名验证失败:X-TIMESTAMP已过期` | 客户端与服务器时间差超过5分钟 | 检查系统时间,使用当前时间戳 |
| `签名校验失败,参数有误!` | 签名值不匹配 | 检查参数排序、JSON序列化格式、密钥是否正确 |
| `code: 1001` | 签名相关错误的统一错误码 | 查看 message 详情 |
## 调试技巧
1. **打印签名输入**:输出 `params_json + secret` 字符串,对比前后端是否一致
2. **对比 JSON 格式**:确保使用 `separators=(',', ':')` 无空格格式
3. **检查类型转换**:数字/布尔/对象必须转为字符串
4. **验证密钥**:确认使用的密钥与服务端配置一致(默认 `dd05f1c54d63749eda95f9fa6d49v442a`