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>防线是结构性的,不是数学上的:任何绕过本数据源另开连接的代码仍能写。
|
* 服务端只读账号(见现场对接清单第七节)是第一道防线,本层是第二道。
|
*/
|
public final class ReadOnlyVendorDataSource implements DataSource
|
{
|
private static final List<String> ALLOWED_PREFIXES = List.of("select", "show", "describe", "desc");
|
|
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)
|
{
|
if (isQuery(sql))
|
{
|
audit.recordForwarded(sql);
|
return;
|
}
|
audit.recordRejected(sql);
|
throw new NonQueryStatementException("厂商数据源只允许查询语句:" + 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;
|
}
|
|
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;
|
}
|
}
|