Skip to content

Commit d6358f5

Browse files
xlorneclaude
andcommitted
fix: 分页包装SQL的结果列无法解析物理表字段导致列权限失效
查询引擎将业务SQL包装为 SELECT 裸列 FROM (...) AS __base__ 形态后, 结果集元数据的表名为派生表别名或空串,无法映射回物理表字段, DataAuthorizationFilter.supportColumnAuthorization 收到错误表名, 列权限/脱敏失效。 修复方式:遍历AST时为派生表(FROM/JOIN子查询)构建"输出列→物理表字段" 投影映射(DerivedTableProjection),支持多层派生链归因与单 * 展开归因; 列权限解析改为联合解析 resolveTableNameAndColumn,命中派生表别名或 表名为空/未知时沿投影归因,已知物理表及歧义场景保持原有行为不变。 移植自 17.3.x 分支 b2b6e9f,已降级为 Java 8 语法(instanceof 模式匹配 改强转、测试 SQL 移至资源文件 issue212.sql)。 Refs #212 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 75a0051 commit d6358f5

8 files changed

Lines changed: 843 additions & 2 deletions

File tree

springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/DataAuthorizationContext.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ public void clearDataAuthorizationFilters() {
6464
*/
6565
public <T> T columnAuthorization(SQLExecuteState interceptState, String tableName, String columnName, T value) {
6666
if (interceptState != null && interceptState.hasIntercept()) {
67-
String realTableName = interceptState.getTableName(tableName);
68-
String realColumnName = interceptState.getColumnName(tableName,columnName);
67+
String[] resolved = interceptState.resolveTableNameAndColumn(tableName, columnName);
68+
String realTableName = resolved[0];
69+
String realColumnName = resolved[1];
6970

7071
for (DataAuthorizationFilter filter : filters) {
7172
if (filter.supportColumnAuthorization(realTableName, realColumnName, value)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.codingapi.springboot.authorization.enhancer;
2+
3+
import java.util.HashMap;
4+
import java.util.HashSet;
5+
import java.util.Map;
6+
import java.util.Set;
7+
8+
/**
9+
* 派生表(FROM/JOIN 子查询)输出列到物理表字段的投影映射。
10+
* <p>
11+
* 解决 Issue #212:分页包装 SQL(SELECT 裸列 FROM (...) AS __base__)执行后,
12+
* 结果集元数据中的表名是派生表别名(或空串),无法直接映射回物理表字段。
13+
* 通过记录派生表每一列的来源(显式列引用 / 唯一 * 展开),实现结果列归因。
14+
*/
15+
class DerivedTableProjection {
16+
17+
/**
18+
* 显式投影列:key = 输出列名(小写),value = [物理表名, 物理字段名]
19+
*/
20+
private final Map<String, String[]> explicitColumns = new HashMap<>();
21+
22+
/**
23+
* 表达式/函数 AS 别名等非物理列派生出的输出列(小写),不参与 * 归因
24+
*/
25+
private final Set<String> expressionColumns = new HashSet<>();
26+
27+
/**
28+
* 唯一 * 来源的物理表名;为 null 且 starAmbiguous 为 false 时表示无 * 投影
29+
*/
30+
private String starTable;
31+
32+
/**
33+
* * 来源是否歧义(多个 * 或多表 SELECT *),歧义时不做 * 归因
34+
*/
35+
private boolean starAmbiguous;
36+
37+
/**
38+
* 添加显式投影列
39+
*
40+
* @param columnKey 输出列名(小写)
41+
* @param tableName 物理表名
42+
* @param columnName 物理字段名
43+
*/
44+
void putExplicit(String columnKey, String tableName, String columnName) {
45+
explicitColumns.put(columnKey, new String[]{tableName, columnName});
46+
}
47+
48+
/**
49+
* 标记非物理列(表达式 AS 别名)
50+
*
51+
* @param columnKey 输出列名(小写)
52+
*/
53+
void markExpression(String columnKey) {
54+
expressionColumns.add(columnKey);
55+
}
56+
57+
/**
58+
* 添加 * 展开来源的物理表;出现多个来源时视为歧义
59+
*
60+
* @param tableName 物理表名,null 表示来源不可解析
61+
*/
62+
void addStarTable(String tableName) {
63+
if (tableName == null || starTable != null) {
64+
starTable = null;
65+
starAmbiguous = true;
66+
} else {
67+
starTable = tableName;
68+
}
69+
}
70+
71+
/**
72+
* 标记 * 来源歧义
73+
*/
74+
void markStarAmbiguous() {
75+
starTable = null;
76+
starAmbiguous = true;
77+
}
78+
79+
/**
80+
* * 展开来源的物理表名(链式解析 t.* 来源时使用)
81+
*/
82+
String getStarTable() {
83+
return starTable;
84+
}
85+
86+
/**
87+
* * 来源是否歧义(链式解析 t.* 来源时使用)
88+
*/
89+
boolean isStarAmbiguous() {
90+
return starAmbiguous;
91+
}
92+
93+
/**
94+
* 将输出列解析为物理表字段
95+
*
96+
* @param columnKey 输出列名(小写)
97+
* @param columnName 输出列名(原样,用于 * 展开归因)
98+
* @return [物理表名, 物理字段名],无法归因时返回 null
99+
*/
100+
String[] find(String columnKey, String columnName) {
101+
String[] reference = explicitColumns.get(columnKey);
102+
if (reference != null) {
103+
return reference;
104+
}
105+
if (expressionColumns.contains(columnKey)) {
106+
return null;
107+
}
108+
if (starTable != null) {
109+
return new String[]{starTable, columnName};
110+
}
111+
return null;
112+
}
113+
}

springboot-starter-data-authorization/src/main/java/com/codingapi/springboot/authorization/enhancer/TableColumnAliasContext.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ public class TableColumnAliasContext {
1818

1919
private final Map<String, String> columnAliasMap;
2020

21+
/**
22+
* 派生表投影:key = 派生表别名(小写)
23+
*/
24+
private final Map<String, DerivedTableProjection> derivedTables;
25+
2126
protected TableColumnAliasContext() {
2227
this.columnAliases = new ArrayList<>();
2328
this.tableAlias = new HashMap<>();
2429
this.columnAliasMap = new HashMap<>();
30+
this.derivedTables = new HashMap<>();
2531
}
2632

2733
/**
@@ -33,6 +39,106 @@ protected void addTable(String tableAlias, String tableName) {
3339
this.tableAlias.put(tableAlias, tableName);
3440
}
3541

42+
/**
43+
* 注册派生表投影
44+
* @param alias 派生表别名
45+
* @param projection 投影映射
46+
*/
47+
protected void addDerivedTable(String alias, DerivedTableProjection projection) {
48+
if (alias != null && projection != null) {
49+
derivedTables.put(alias.toLowerCase(), projection);
50+
}
51+
}
52+
53+
/**
54+
* 按表引用名(表名或别名,忽略大小写)获取物理表名
55+
* @param tableRef 表引用名
56+
* @return 物理表名,未找到返回 null
57+
*/
58+
protected String resolvePhysicalTable(String tableRef) {
59+
if (tableRef == null || tableRef.isEmpty()) {
60+
return null;
61+
}
62+
for (Map.Entry<String, String> entry : tableAlias.entrySet()) {
63+
if (entry.getKey().equalsIgnoreCase(tableRef)) {
64+
return entry.getValue();
65+
}
66+
}
67+
for (String tableName : tableAlias.values()) {
68+
if (tableName.equalsIgnoreCase(tableRef)) {
69+
return tableName;
70+
}
71+
}
72+
return null;
73+
}
74+
75+
/**
76+
* 获取派生表投影(忽略大小写)
77+
* @param tableRef 派生表别名
78+
* @return 投影映射,未找到返回 null
79+
*/
80+
protected DerivedTableProjection getDerivedTable(String tableRef) {
81+
if (tableRef == null || tableRef.isEmpty()) {
82+
return null;
83+
}
84+
return derivedTables.get(tableRef.toLowerCase());
85+
}
86+
87+
/**
88+
* 联合解析结果集列的(表名, 字段名)到物理表字段。
89+
* <p>
90+
* 元数据表名命中派生表别名、或为空/未知(部分数据库对派生表列上报空表名,
91+
* 见 Issue #212)时,沿派生表投影归因;否则回退到既有解析行为。
92+
*
93+
* @param tableName 元数据表名(或别名)
94+
* @param columnName 元数据字段名(或别名)
95+
* @return [物理表名, 物理字段名]
96+
*/
97+
public String[] resolveTableNameAndColumn(String tableName, String columnName) {
98+
String[] derived = resolveDerivedColumn(tableName, columnName);
99+
if (derived != null) {
100+
return derived;
101+
}
102+
return new String[]{getTableName(tableName), getColumnName(tableName, columnName)};
103+
}
104+
105+
private String[] resolveDerivedColumn(String tableName, String columnName) {
106+
if (derivedTables.isEmpty() || columnName == null || columnName.isEmpty()) {
107+
return null;
108+
}
109+
String columnKey = columnName.toLowerCase();
110+
String tableKey = tableName == null ? "" : tableName.toLowerCase();
111+
if (!tableKey.isEmpty()) {
112+
DerivedTableProjection projection = derivedTables.get(tableKey);
113+
if (projection != null) {
114+
return projection.find(columnKey, columnName);
115+
}
116+
// 已知物理表/别名:维持原有解析,不做派生归因,避免误匹配
117+
for (String alias : tableAlias.keySet()) {
118+
if (alias.equalsIgnoreCase(tableKey)) {
119+
return null;
120+
}
121+
}
122+
for (String physicalTable : tableAlias.values()) {
123+
if (physicalTable.equalsIgnoreCase(tableKey)) {
124+
return null;
125+
}
126+
}
127+
}
128+
// 表名为空或未知:在所有派生表投影中做唯一匹配(匹配到多个不同物理表时不归因)
129+
String[] matched = null;
130+
for (DerivedTableProjection projection : derivedTables.values()) {
131+
String[] reference = projection.find(columnKey, columnName);
132+
if (reference != null) {
133+
if (matched != null && !matched[0].equalsIgnoreCase(reference[0])) {
134+
return null;
135+
}
136+
matched = reference;
137+
}
138+
}
139+
return matched;
140+
}
141+
36142
/**
37143
* 添加字段别名
38144
* @param parent 父级(上级别名)

0 commit comments

Comments
 (0)