46. APIJSON 低代码接口
一句话定位:APIJSON 让你用一段 JSON 描述"查哪张表、哪些字段、什么条件、怎么分页",由 APIJSONService 翻译成 SqlSugar 查询返回 JSON,前端不用等后端写接口。它是"后台配表即可_query_ 数据"的加速器,但也是权限风险最高的一环。
一、结构与职责
Admin.NET.Core/Service/APIJSON/
├── APIJSONService.cs 对外动态 API(get / get/{table} / add / update / delete)
├── IdentityService.cs 取当前身份→角色(APIJSON_Role),判断表/列权限
├── SelectTable.cs 查询执行引擎(表名/列名校验、字段投影、分页、条件、函数调用)
├── TableMapper.cs 表名 ↔ 实体类型映射
├── FuncList.cs 可在 JSON 里调用的辅助函数
└── 用例APIFOX.json 可直接导入 Apifox 的接口用例集注册:
// Admin.NET.Web.Core/ProjectOptions.cs
services.AddConfigurableOptions<APIJSONOptions>();
// 服务本体
[ApiDescriptionSettings(Order = 100, Description = "APIJSON")]
public class APIJSONService : IDynamicApiController, ITransient
{
public APIJSONService(ISqlSugarClient db, IdentityService identityService, TableMapper tableMapper)
{
_db = db; _tableMapper = tableMapper; _identityService = identityService;
_selectTable = new SelectTable(_identityService, _tableMapper, _db);
}
}由此得到的四个接口:
| 方法 | 路由 | 作用 |
|---|---|---|
Query | POST /api/apijson/get | 统一查询入口(支持 @database 指定多库) |
QueryByTable | POST /api/apijson/get/{table} | 单表查询(推荐) |
Add | POST /api/apijson/add | 新增(支持批量,受插入权限约束) |
Edit | POST /api/apijson/update | 更新(只支持 Id 作为条件) |
Delete | POST /api/apijson/delete | 删除(支持非 Id 条件、批量) |
二、配置
// Admin.NET.Application/Configuration/APIJSON.json
{
"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
"APIJSON": {
"Roles": [
{
"RoleName": "Role1", // 权限名称 唯一
"Select": { // 查询
"Table": [ "*" ], // 可操作的表
"Column": [ "*" ], // 可操作的字段
"Filter": []
},
"Insert": { // 添加
"Table": [ "table1", "table2", "table3" ],
"Column": [ "*", "*", "tb.*" ] // 与 Table 按【下标】对应
},
"Update": { // 修改
"Table": [ "table1", "table2", "table3" ],
"Column": [ "*", "tb.*", "tb.*" ]
},
"Delete": { // 删除
"Table": [ "table1", "table2", "table3" ]
}
},
{
"RoleName": "Role2",
"Select": {
"Table": [ "table1" ],
"Column": [ "tb.*" ]
}
}
]
}
}Column 与 Table 是按下标一一对应的,不是两个独立的集合。写错行数会错位放行。
Select.Column 取值含义:
| 值 | 含义 |
|---|---|
"*" | 该表所有列都可读 |
"tb.*" | (配合通配符用法)按表前缀放行 |
| 具体列名数组 | 只能读这些列(其实我们的实现是一整个字符串做包含判断,见下) |
权限判定源码(IdentityService):
public string GetUserRoleName()
=> _context.HttpContext.User.FindFirstValue(ClaimTypes.Role); // 从 ClaimTypes.Role 取
public APIJSON_Role GetRole()
{
var role = string.IsNullOrEmpty(GetUserRoleName())
? _roles.FirstOrDefault() // 没带角色时取配置里的第一个!
: _roles.FirstOrDefault(it => it.RoleName.Equals(GetUserRoleName(), StringComparison.CurrentCultureIgnoreCase));
return role;
}
public (bool, string) GetSelectRole(string table)
{
var role = GetRole();
if (role == null || role.Select == null || role.Select.Table == null)
return (false, "appsettings.json权限配置不正确!");
var tablerole = role.Select.Table.FirstOrDefault(it => it == "*" || it.Equals(table, StringComparison.CurrentCultureIgnoreCase));
if (string.IsNullOrEmpty(tablerole)) return (false, $"表名{table}没权限查询!");
var index = Array.IndexOf(role.Select.Table, tablerole);
return (true, role.Select.Column[index]); // ← 按下标取 Column
}⚠️ 最关键的一句:没有 ClaimTypes.Role 这个 Claim 的用户会自动落到 _roles.FirstOrDefault(),也就是配置里写的第一个角色。默认模板第一个 Role1 的 Table: ["*"]、Column: ["*"] —— 相当于全库可读。生产务必改。
三、请求示例
3.1 单表分页查询
curl -X POST 'http://localhost:5005/api/apijson/get/SysLogOp' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
"page": 1,
"count": 20,
"query": 2,
"Account": "admin"
}'服务端处理(QueryByTable):
var ht = new JObject { { table + "[]", jobject } };
// 自动添加总计数量
if (jobject["query"] != null && jobject["query"].ToString() != "0" && jobject["total@"] == null)
ht.Add("total@", "");
// 每页最大1000条数据
if (jobject["count"] != null && int.Parse(jobject["count"].ToString()) > 1000)
throw Oops.Bah("count分页数量最大不能超过1000");
jobject.Remove("@debug");
var ignoreConditions = new List<string> { "page", "count", "query" }; // 这三个不当作字段条件响应:
{
"SysLogOp[]": [
{ "Id": "...", "Account": "admin", "RealName": "超管", "CreateTime": "2026-10-18 10:00:00" }
],
"total@": 1,
"code": 200,
"type": "success",
"message": "",
"time": "2026-10-18T10:12:33"
}3.2 统一入口 + 指定数据库
curl -X POST 'http://localhost:5005/api/apijson/get' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{ "@database": "LogDb", "[]": { "SYSLOGOP": { "page": 1, "count": 10 } } }'var database = jobject["@database"]?.ToString();
if (!string.IsNullOrEmpty(database))
{
var provider = _db.AsTenant().GetConnectionScope(database); // 指定多库连接
jobject.Remove("@database");
return new SelectTable(_identityService, _tableMapper, provider).Query(jobject);
}3.3 新增(单条 / 批量)
curl -X POST 'http://localhost:5005/api/apijson/add' \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{ "BizItem": { "Title": "测试", "Qty": 3 } }'# 批量:值用数组
-d '{ "BizItem": [ { "Title": "A" }, { "Title": "B" } ] }'返回:{ "BizItem": { "id": [...], "count": 2 } }(未传 Id 时后端用雪花 Id 生成)。
3.4 更新(只支持 Id 作为条件)
curl -X POST 'http://localhost:5005/api/apijson/update' \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{ "BizItem": { "Id": 123456789, "Title": "改标题" } }'3.5 删除(支持非 Id 条件、批量)
curl -X POST 'http://localhost:5005/api/apijson/delete' \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{ "BizItem": { "Title": "测试" } }'删除是物理删除(
_db.Deleteable<object>().AS(tableName).Where(whereSql, parameters)),不走软删除!执行前先确认Delete.Table白名单。
四、字段/表名校验
SelectTable 会先校验元数据,把错误前置:
public virtual bool IsTable(string table)
=> _db.DbMaintenance.GetTableInfoList().Any(it => it.Name.Equals(table, StringComparison.CurrentCultureIgnoreCase))
? true : throw new Exception($"表名【{table}】不正确!");
public virtual bool IsCol(string table, string col)
=> _db.DbMaintenance.GetColumnInfosByTableName(table).Any(it => it.DbColumnName.Equals(col, StringComparison.CurrentCultureIgnoreCase))
? true : throw new Exception($"表【{table}】不存在列【{col}】!请检查输入参数");由此带来的特点:表结构变更即时生效,因为直接读数据库元数据,不需要实体类。这也是它能被用于动态表/报表场景的原因。
五、适用场景
| 场景 | 合适度 | 说明 |
|---|---|---|
| 内部管理后台的临时查数 | ⭐⭐⭐⭐⭐ | 不用写接口,前端直接拼 JSON |
| 动态表单 / 用户自定义表 | ⭐⭐⭐⭐⭐ | 元数据驱动,字段变更零改造 |
| 大屏/报表的取数 | ⭐⭐⭐⭐ | 配合 @database 可跨库 |
| 移动端简单 CRUD | ⭐⭐⭐ | 注意 count 上限和返回体大小 |
| 开放给第三方 | ⭐ | 强烈不建议,见风险一节 |
| 高并发核心交易链路 | ⭐ | 无缓存、无针对性索引优化 |
FuncList 提供了在 JSON 里调用辅助函数的能力(SelectTable.ExecFunc → 反射调 typeof(FuncList).GetMethod(funcname)),用于 date/string 之类的简单转换。
六、风险与红线
- 默认配置等于全库可读:见第二节的
GetRole()回落逻辑。第一件事就是把Roles里的"*"换成真实表名白名单。 - 不走第 13 篇那套数据范围过滤器:
SelectTable直接用ISqlSugarClient拼查询,没有租户/软删除/机构范围过滤。这意味着跨租户数据泄漏是可行的。绝不要把带TenantId敏感表放进白名单。 - APIJSON 与普通 RBAC 是两套体系:它只认
ClaimTypes.Role+APIJSON.json,不认菜单按钮权限码。要收紧请在JWT payload里写 role,或改IdentityService.GetRole()。 - 删除是物理删除:
Deleteable<object>().AS(table)直接硬删,软删除标志、审计、触发器记录全都没有。 - SQL 注入面:虽然条件值走
SugarParameter参数化,但表名/列名是拼进 SQL 的(AS(tableName)、Where(whereSql))。依赖的是IsTable/IsCol的白名单校验,一旦有人绕过就是注入。 count上限 1000 是唯一的分页保护:没有行数/超时双保险,宽表 + 大 count 会拖垮连接。- 返回体包含全部列:外泄
Password、IdCardNum这类字段只是改一行配置的事。禁止把SysUser、SysOpenAccess、SysConfig放进Select.Table。 - 写操作同样只按表名授权:给了
Insert/Update/Delete就等于允许任意人写这张表的所有列(除Column约束外)。 - 不在 Swagger 分组可见范围内也能被调用:
ApiDescriptionSettings(Order = 100)只是文档分组,隐藏不等于禁用。
安全基线建议:
"APIJSON": {
"Roles": [
{
// 兜底角色:只给一张安全的字典表,只读
"RoleName": "Default",
"Select": { "Table": [ "SysDictData" ], "Column": [ "Id,Code,Value,OrderNo" ], "Filter": [] }
},
{
// 业务角色:显式表名白名单,Columns 精确到列
"RoleName": "BizReadOnly",
"Select": { "Table": [ "BizVisitReport" ], "Column": [ "Id,Title,VisitTime,Owner" ] }
}
]
}并把这套接口从对外路由里摘掉(Nginx 层禁用 /api/apijson/**),或者至少只在内网开放。
七、步骤清单
APIJSON.json里把Roles改成真实白名单,第一个角色尤其要紧- 确认
APIJSONOptions已注册(ProjectOptions.AddProjectOptions) - 给实际使用者的账号配置
ClaimTypes.Role(否则落第一个角色) - 先用
get/{table}跑通单表查询,逐步加page/count/条件 - 写操作前先确认
Insert/Update/Delete.Table白名单 - 用
Admin.NET.Core/Service/APIJSON/用例APIFOX.json导入 Apifox 快速联调 - 生产上线前:Nginx 层限制来源 + 审计日志核对(
LoggingMonitor会记录请求体) - 定期复查:
Select.Table里出现["*"]就是事故本体
⚠️ 注意事项 / 常见坑
QueryByTable里count > 1000抛的是Oops.Bah:出参message = "count分页数量最大不能超过1000",别当成业务异常去查错误码。total@只在query != 0时自动补:query: 0表示"只要原始数据不要总数",前端要自己处理缺失total@的情况。@debug被强制移除:源码jobject.Remove("@debug"),本地想调试别指望这个键。page/count/query是保留键:不会当作表字段条件,其余非保留键会被当作"表的其它查询条件"组装。SelectTable是ISingleton但持有IdentityService(ITransient):这是构造注入捕获的依赖,多用户并发时不会串号(IdentityService内部靠IHttpContextAccessor),但别在里面加任何用户态字段。- 多库查询要传
@database:值必须是DbConnection配置里的ConfigId,写错会抛连接不存在。 - 表/列的大小写:统一走
OrdinalIgnoreCase比较,但返回的 JSON key 用的是数据库实际列名(下划线风格),前端别用驼峰取。 Column的判空:GetSelectRole返回(false, 提示)时,调用方要处理失败分支;漏了就是默认放行。- Dto 不参与:APIJSON 完全绕过Dto 校验(
[Required]、[MaxLength]都无效),数据完整性只能靠数据库约束。 - 审计仍可用:请求会进
LoggingMonitor/SysLogOp,所以每一次越权查询都有留痕——但也意味着请求体里的敏感值会被写进日志。
小结
- 四个写接口 + 两个查接口,
Admin.NET.Core/Service/APIJSON/全实现,配置在Configuration/APIJSON.json - 登录态的产物是
ClaimTypes.Role→ 映射到APIJSON.Roles[].RoleName,取不到就回落到第一个角色 Select.Table/Select.Column按下标一一对应,"*"就是全放行count上限 1000、total@需query != 0、删除是物理删除- 最大风险:绕过第 13 篇的数据范围过滤,且大多数教程会直接复制
"*"模板 - 定位建议:内部查数/动态表单可用,对外开放必须禁