| | |
| | | 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 void requireQuery(String sql) |
| | | { |
| | | if (isQuery(sql)) |
| | | boolean query = isQuery(sql); |
| | | boolean locking = query && containsLockingClause(sql); |
| | | if (query && !locking) |
| | | { |
| | | audit.recordForwarded(sql); |
| | | return; |
| | | } |
| | | audit.recordRejected(sql); |
| | | throw new NonQueryStatementException("厂商数据源只允许查询语句:" + abbreviate(sql)); |
| | | throw new NonQueryStatementException(locking |
| | | ? "厂商数据源禁止加锁读(会阻塞厂商写入):" + abbreviate(sql) |
| | | : "厂商数据源只允许查询语句:" + abbreviate(sql)); |
| | | } |
| | | |
| | | /** 只按语句首关键字判定,避免用正则近似 SQL 语法。 */ |
| | |
| | | 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(); |