如何用JSQLParser的ExpressionDeParser实现MySQL字段加解密拦截?
刚好之前做过类似的需求,用JSQLParser来实现SQL的加密解密拦截完全可行,核心是通过自定义AST访问器(Visitor)来识别并修改特定的SQL节点。下面给你一步步拆解实现方案:
核心思路
JSQLParser会把SQL解析成抽象语法树(AST),我们只需要遍历这个树,找到目标字段对应的节点:
- 对于INSERT/UPDATE,把目标字段对应的JDBC参数(
?)替换成AES_ENCRYPT(?, 密钥) - 对于SELECT,把目标字段替换成
AES_DECRYPT(字段名, 密钥) AS 原字段名,保证上层业务拿到的字段名不变
一、处理INSERT/UPDATE语句(加密参数)
我们需要自定义StatementDeParser的子类,重写visit(Insert)和visit(Update)方法,定位到目标字段对应的参数并替换:
- INSERT中:先找到目标字段在列列表中的索引,再找到VALUES中对应位置的
?,替换为加密函数 - UPDATE中:遍历SET子句的键值对,找到目标字段对应的赋值参数,替换为加密函数
二、处理SELECT语句(解密字段)
同样在自定义Visitor中重写visit(Select)方法,通过SelectDeParser遍历SELECT的列项,把目标字段替换为解密函数,并保留原字段别名。
三、完整可运行示例代码
import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.expression.JdbcParameter; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectExpressionItem; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.util.deparser.SelectDeParser; import net.sf.jsqlparser.util.deparser.StatementDeParser; import java.util.List; public class SqlEncryptDecryptHandler extends StatementDeParser { // 要加密/解密的目标字段 private static final String TARGET_COLUMN = "name"; // AES加密密钥(建议从配置中心读取,不要硬编码) private static final String AES_SECRET_KEY = "your_secure_key_here"; public SqlEncryptDecryptHandler() { super(new net.sf.jsqlparser.util.deparser.ExpressionDeParser(), new StringBuffer()); } // 处理INSERT语句 @Override public void visit(Insert insert) { List<String> columns = insert.getColumns(); if (columns != null && columns.contains(TARGET_COLUMN)) { int targetIndex = columns.indexOf(TARGET_COLUMN); // 遍历所有VALUES组(支持批量插入) for (List<Expression> valueGroup : insert.getValues().getExpressions()) { if (targetIndex < valueGroup.size()) { Expression param = valueGroup.get(targetIndex); if (param instanceof JdbcParameter) { // 构建AES_ENCRYPT函数 Function aesEncrypt = new Function(); aesEncrypt.setName("AES_ENCRYPT"); aesEncrypt.addParameter(param); aesEncrypt.addParameter(buildKeyExpression()); valueGroup.set(targetIndex, aesEncrypt); } } } } super.visit(insert); } // 处理UPDATE语句 @Override public void visit(Update update) { List<Expression> setItems = update.getSetExpressions(); // SET子句是[列, 值, 列, 值...]的结构,步长为2遍历 for (int i = 0; i < setItems.size(); i += 2) { Expression columnExpr = setItems.get(i); String columnName = columnExpr.toString(); if (TARGET_COLUMN.equals(columnName)) { Expression valueExpr = setItems.get(i + 1); if (valueExpr instanceof JdbcParameter) { Function aesEncrypt = new Function(); aesEncrypt.setName("AES_ENCRYPT"); aesEncrypt.addParameter(valueExpr); aesEncrypt.addParameter(buildKeyExpression()); setItems.set(i + 1, aesEncrypt); } } } super.visit(update); } // 处理SELECT语句 @Override public void visit(Select select) { SelectDeParser selectDeParser = new SelectDeParser(getExpressionVisitor(), getBuffer()) { @Override public void visit(SelectExpressionItem item) { Expression expr = item.getExpression(); String columnName = expr.toString(); if (TARGET_COLUMN.equals(columnName)) { // 构建AES_DECRYPT函数,保留原别名 Function aesDecrypt = new Function(); aesDecrypt.setName("AES_DECRYPT"); aesDecrypt.addParameter(expr); aesDecrypt.addParameter(buildKeyExpression()); item.setExpression(aesDecrypt); if (item.getAlias() == null) { item.setAlias(TARGET_COLUMN); } } super.visit(item); } }; select.getSelectBody().accept(selectDeParser); } // 构建密钥表达式(避免重复代码) private Expression buildKeyExpression() { try { return CCJSqlParserUtil.parseExpression("'" + AES_SECRET_KEY + "'"); } catch (JSQLParserException e) { throw new RuntimeException("Failed to build AES key expression", e); } } // 对外暴露的SQL处理方法 public static String processSql(String originalSql) throws JSQLParserException { Statement statement = CCJSqlParserUtil.parse(originalSql); SqlEncryptDecryptHandler handler = new SqlEncryptDecryptHandler(); statement.accept(handler); return handler.getBuffer().toString(); } // 测试用例 public static void main(String[] args) throws JSQLParserException { // 测试INSERT String insertSql = "insert into customer (id, name) values (?,?)"; System.out.println("原INSERT: " + insertSql); System.out.println("处理后INSERT: " + processSql(insertSql)); // 输出: insert into customer (id, name) values (?, AES_ENCRYPT(?, 'your_secure_key_here')) // 测试UPDATE String updateSql = "update customer set name = ? where id = ?"; System.out.println("\n原UPDATE: " + updateSql); System.out.println("处理后UPDATE: " + processSql(updateSql)); // 输出: update customer set name = AES_ENCRYPT(?, 'your_secure_key_here') where id = ? // 测试SELECT String selectSql = "SELECT id, name FROM customer where id = ?"; System.out.println("\n原SELECT: " + selectSql); System.out.println("处理后SELECT: " + processSql(selectSql)); // 输出: SELECT id, AES_DECRYPT(name, 'your_secure_key_here') AS name FROM customer where id = ? } }
注意事项
- 密钥安全:绝对不要硬编码密钥,建议从配置中心、环境变量或密钥管理服务读取
- 复杂场景兼容:如果你的SQL包含表别名(比如
SELECT c.name FROM customer c)、子查询、批量插入多组值等,需要扩展代码中的字段匹配逻辑,比如解析表别名后再判断字段名 - 性能优化:AST解析有一定开销,建议只针对需要加密的表和字段做处理,避免全量SQL解析
- 异常处理:代码中可以添加更多的空值判断和异常捕获,避免解析异常导致业务中断
内容的提问来源于stack exchange,提问作者Ardile
相关产品推荐
相关产品推荐

