yeg
6 天以前 e38aca8134570513435c003c25dc05f1ae51c9c9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
package com.battery.device.neware;
 
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import javax.sql.DataSource;
 
/**
 * 厂商数据源的只读防线:在 JDBC 调用到达驱动之前按语句白名单放行。
 *
 * <p>只允许 {@code SELECT} / {@code SHOW} / {@code DESCRIBE}(含 {@code DESC});
 * 其余语句(含存储过程调用)直接抛 {@link NonQueryStatementException},不转发给驱动,
 * 因此在服务端不产生任何效果。被拒绝的语句记入 {@link ReadOnlyStatementAudit},
 * 使"该数据源从未发出过非查询语句"可以被测试断言。
 *
 * <p>防线是结构性的,不是数学上的:任何绕过本数据源另开连接的代码仍能写。
 * 服务端只读账号(见现场对接清单第七节)是第一道防线,本层是第二道。
 *
 * <p>{@link DataSource} 接口中与只读边界无关的 JDK 样板方法(日志写出器、登录超时、
 * 父日志器、{@code unwrap} / {@code isWrapperFor} / {@code toString})直接转发或拒绝驱动对象,
 * 不承载业务语义,因此不单独加方法注释。
 */
public final class ReadOnlyVendorDataSource implements DataSource
{
    private static final List<String> ALLOWED_PREFIXES = List.of("select", "show", "describe", "desc");
    /**
     * 加锁读会真的阻塞厂商写入,所以它虽然以 {@code select} 开头也要单独拒绝:
     * 现场实测厂商库 {@code tx_isolation=REPEATABLE-READ},行锁会挡住上位机的写库。
     */
    private static final List<String> LOCKING_CLAUSES = List.of("for update", "for share",
            "lock in share mode", "updlock", "holdlock", "xlock", "tablock");
 
    private final DataSource delegate;
    private final String sourceLabel;
    private final ReadOnlyStatementAudit audit = new ReadOnlyStatementAudit();
 
    public ReadOnlyVendorDataSource(DataSource delegate, String sourceLabel)
    {
        if (delegate == null) throw new IllegalArgumentException("delegate data source must not be null");
        this.delegate = delegate;
        this.sourceLabel = sourceLabel == null || sourceLabel.isBlank() ? "vendor" : sourceLabel.trim();
    }
 
    /** 返回本数据源上的语句审计,用于断言从未发出过非查询语句。 */
    public ReadOnlyStatementAudit audit()
    {
        return audit;
    }
 
    /** 取一条受只读约束的连接;语句白名单在返回的连接代理上生效。 */
    @Override
    public Connection getConnection() throws SQLException
    {
        return guard(delegate.getConnection());
    }
 
    /** 同上,按给定账号取连接;账号仍受只读白名单约束。 */
    @Override
    public Connection getConnection(String username, String password) throws SQLException
    {
        return guard(delegate.getConnection(username, password));
    }
 
    private Connection guard(Connection connection)
    {
        return (Connection) Proxy.newProxyInstance(connection.getClass().getClassLoader(),
                new Class<?>[] { Connection.class }, (proxy, method, args) -> {
                    switch (method.getName())
                    {
                        case "prepareStatement" ->
                        {
                            String sql = (String) args[0];
                            requireQuery(sql);
                            return guardStatement((Statement) invoke(connection, method, args), sql, true);
                        }
                        case "prepareCall" -> throw new NonQueryStatementException(
                                "厂商数据源禁止存储过程调用:" + abbreviate((String) args[0]));
                        case "createStatement" ->
                        {
                            return guardStatement((Statement) invoke(connection, method, args), null, false);
                        }
                        default -> { }
                    }
                    return invoke(connection, method, args);
                });
    }
 
    private Statement guardStatement(Statement statement, String preparedSql, boolean prepared)
    {
        Class<?> statementType = prepared ? PreparedStatement.class : Statement.class;
        return (Statement) Proxy.newProxyInstance(statement.getClass().getClassLoader(),
                new Class<?>[] { statementType }, (proxy, method, args) -> {
                    String sql = sqlArgument(method, args);
                    if (sql != null) requireQuery(sql);
                    return invoke(statement, method, args);
                });
    }
 
    /**
     * 取出本次调用携带的 SQL 文本:语句级方法带 SQL 时按该 SQL 判定,
     * 预编译语句的无参执行按 prepare 时的 SQL 判定(已在 prepareStatement 中放行)。
     */
    private static String sqlArgument(Method method, Object[] args)
    {
        if (args == null || args.length == 0 || !(args[0] instanceof String sql)) return null;
        return switch (method.getName())
        {
            case "execute", "executeUpdate", "executeLargeUpdate", "executeQuery", "addBatch", "executeBatch",
                    "executeLargeBatch" -> sql;
            default -> null;
        };
    }
 
    private void requireQuery(String sql)
    {
        boolean query = isQuery(sql);
        boolean locking = query && containsLockingClause(sql);
        if (query && !locking)
        {
            audit.recordForwarded(sql);
            return;
        }
        audit.recordRejected(sql);
        throw new NonQueryStatementException(locking
                ? "厂商数据源禁止加锁读(会阻塞厂商写入):" + abbreviate(sql)
                : "厂商数据源只允许查询语句:" + abbreviate(sql));
    }
 
    /** 只按语句首关键字判定,避免用正则近似 SQL 语法。 */
    static boolean isQuery(String sql)
    {
        if (sql == null) return false;
        String normalized = stripLeadingComments(sql).toLowerCase(Locale.ROOT);
        if (normalized.isEmpty()) return false;
        for (String prefix : ALLOWED_PREFIXES)
        {
            if (!normalized.startsWith(prefix)) continue;
            int boundary = prefix.length();
            if (normalized.length() == boundary || Character.isWhitespace(normalized.charAt(boundary))
                    || normalized.charAt(boundary) == '(' || normalized.charAt(boundary) == '*'
                    || normalized.charAt(boundary) == '`')
                return true;
        }
        return false;
    }
 
    /**
     * 语句里是否带加锁读子句。这是**语法层的拦截**,不是 SQL 解析器:厂商取数的语句由本模块固定给出,
     * 不会把加锁子句藏在字符串常量里;新增取数语句时由 {@code ReadOnlyVendorDataSourceTest} 一起覆盖。
     */
    static boolean containsLockingClause(String sql)
    {
        if (sql == null) return false;
        String normalized = stripLeadingComments(sql).toLowerCase(Locale.ROOT).replaceAll("\\s+", " ");
        for (String clause : LOCKING_CLAUSES)
        {
            if (normalized.contains(clause)) return true;
        }
        return false;
    }
 
    private static String stripLeadingComments(String sql)
    {
        String remaining = sql.trim();
        while (true)
        {
            if (remaining.startsWith("/*"))
            {
                int end = remaining.indexOf("*/");
                if (end < 0) return remaining;
                remaining = remaining.substring(end + 2).trim();
                continue;
            }
            if (remaining.startsWith("--") || remaining.startsWith("#"))
            {
                int end = remaining.indexOf('\n');
                if (end < 0) return "";
                remaining = remaining.substring(end + 1).trim();
                continue;
            }
            return remaining;
        }
    }
 
    private static String abbreviate(String sql)
    {
        if (sql == null) return "";
        String trimmed = sql.trim();
        return trimmed.length() <= 200 ? trimmed : trimmed.substring(0, 200);
    }
 
    private static Object invoke(Object target, Method method, Object[] args) throws Throwable
    {
        try
        {
            return method.invoke(target, args);
        }
        catch (InvocationTargetException exception)
        {
            throw exception.getTargetException();
        }
    }
 
    @Override
    public PrintWriter getLogWriter() throws SQLException
    {
        return delegate.getLogWriter();
    }
 
    @Override
    public void setLogWriter(PrintWriter out) throws SQLException
    {
        delegate.setLogWriter(out);
    }
 
    @Override
    public void setLoginTimeout(int seconds) throws SQLException
    {
        delegate.setLoginTimeout(seconds);
    }
 
    @Override
    public int getLoginTimeout() throws SQLException
    {
        return delegate.getLoginTimeout();
    }
 
    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException
    {
        return delegate.getParentLogger();
    }
 
    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException
    {
        if (iface.isInstance(this)) return iface.cast(this);
        throw new SQLFeatureNotSupportedException("厂商数据源不暴露未受约束的驱动对象");
    }
 
    @Override
    public boolean isWrapperFor(Class<?> iface)
    {
        return iface.isInstance(this);
    }
 
    @Override
    public String toString()
    {
        return "ReadOnlyVendorDataSource[" + sourceLabel + "]";
    }
 
    /** 便于测试与排查的来源说明。 */
    public String sourceLabel()
    {
        return sourceLabel;
    }
}