动态 SQL 模板里,权限条件为什么要注入而不是散落在业务代码里

前面几篇一直在讲一个很早期的问题:Spring JPA / Hibernate 项目里,复杂列表、报表、导出这些查询,经常需要写动态 SQL。

一开始我们关心的是 SQL 怎么别写成一堆 Java 字符串拼接。后来发现,AI 时代下,LLM 很适合生成这种 SQL 模板;再往前走一步,就会遇到另一个更实际的问题:权限条件到底写在哪里。

在企业系统里,很多查询不是简单的"按页面参数筛选"。

它通常还要带上这些东西:

  • 当前用户是谁;
  • 当前租户或公司是什么;
  • 用户能看哪些团队、部门、仓库;
  • 是否只能看自己创建的数据;
  • 是否需要叠加某些业务状态或组织边界。

如果每个查询都自己写一遍,这些条件很快就会散掉。

比如一个订单列表里写:

java 复制代码
if (!currentUser.isAdmin()) {
    sql.append(" and o.team_id in (");
    sql.append(teamIds.stream().map(x -> "?").collect(joining(",")));
    sql.append(")");
    args.addAll(teamIds);
}

另一个导出接口里写:

java 复制代码
if (userContext.hasTenant()) {
    sql.append(" and tenant_id = ?");
    args.add(userContext.getTenantId());
}

这些代码单看都能理解,但久了以后会出现几个问题。

第一,权限条件和业务筛选混在一起。开发者很难一眼看出哪些是页面条件,哪些是系统必须带上的数据范围。

第二,不同查询的写法会不一致。有的地方用了租户,有的地方忘了租户;有的地方按团队,有的地方按创建人;有的地方空团队列表返回全部,有的地方返回空结果。

第三,AI 帮忙改查询时,也很容易漏。LLM 看到的是"订单列表增加一个状态筛选",它未必知道旁边那段团队范围其实不能动。

所以到了 SQL 模板阶段,我们很自然会想:权限上下文能不能也从 Spring 里接进来,让模板里只保留清晰的调用点?

比如:

javascript 复制代码
import {getCurrentUser, getDataScope} from '@authService';

const user = getCurrentUser();
const scope = getDataScope('order');

export const sql = `
    SELECT
        o.order_id,
        o.order_no,
        o.amount,
        o.status,
        o.create_time
    FROM orders o
    WHERE o.deleted = 0
        ${sqlExp(user.tenantId, 'AND o.tenant_id = ?', true)}
        ${sqlInExp(scope.teamIds, 'AND o.team_id IN ', true)}
        ${sqlExp(form.param.status, 'AND o.status = ?')}
        ${sqlExp(toLikeStr(form.param.keyword), 'AND o.order_no LIKE ?')}
    ORDER BY o.create_time DESC
`;

这里重点不是 authService 这个名字,而是思路:权限上下文由 Spring Bean 提供,模板只负责把它放到查询里。

这样做有几个好处。

第一个好处是,权限条件有了明确入口。

代码评审时,一眼能看到:

javascript 复制代码
const user = getCurrentUser();
const scope = getDataScope('order');

以及:

javascript 复制代码
${sqlExp(user.tenantId, 'AND o.tenant_id = ?', true)}
${sqlInExp(scope.teamIds, 'AND o.team_id IN ', true)}

这比在 Java 业务代码里翻一堆 ifappendargs.add 更直接。

第二个好处是,权限逻辑可以集中。

比如"订单数据范围"到底是团队、部门、仓库,还是"自己 + 下属",这个判断不应该分散在每个 SQL 模板里。模板应该拿到一个已经整理好的 scope,再用固定 helper 注入查询。

真正复杂的权限判断仍然留在 Java 服务里:

java 复制代码
public DataScope getDataScope(String resource) {
    // 根据当前用户、角色、组织、业务资源计算可访问范围
}

模板只处理 SQL 表达:

javascript 复制代码
${sqlInExp(scope.teamIds, 'AND o.team_id IN ', true)}

这就把"权限怎么计算"和"SQL 怎么表达"分开了。

第三个好处是,AI 更不容易误改。

如果提示词里明确说:

text 复制代码
权限条件来自 getCurrentUser() 和 getDataScope(resource)。
这些条件必须保留,不要删除或改成页面参数。
新增页面筛选时,只能追加 form.param 相关条件。

LLM 生成或修改模板时,就有比较清晰的边界。它可以帮忙加状态、关键字、时间范围,但不应该随手删掉租户和数据范围。

这对人工审查也有帮助。审查者不用猜"这段 team_id 是不是权限条件",因为权限相关调用已经有稳定形态。

当然,这一步仍然不是完整权限治理。

它不能保证所有模型都有正确权限,也不能自动解决复杂的多角色、多组织、多公司规则。更不能说"有了模板注入,就可以让 AI 随便查库"。

它解决的是一个更早、更朴素的问题:在 Spring 企业项目里,权限条件不要散落在每个 Repository、Service、JdbcTemplate 拼接分支里。

先让当前用户、租户、团队范围这些上下文有统一入口,再让 SQL 模板以固定方式引用它们。

这样一来,动态 SQL 模板不只是"写起来更舒服",也开始有了一点工程边界:

  • 页面筛选来自 form.param
  • 权限上下文来自 Spring Bean;
  • SQL 参数通过 helper 收集;
  • 必带条件用 force 明确表达;
  • 调试时能看到最终 SQL 和参数。

这一步仍然很小,但它已经开始把复杂查询从"谁写谁负责"往"团队有规则可遵守"推进。

对 AI 辅助开发来说,这个变化也很关键。LLM 不需要理解你们公司完整的权限系统,但它需要知道:有些条件不是它自由发挥的业务筛选,而是必须保留的访问边界。

相关代码:foggy-dataset Java 引擎模块

github.com/foggy-proje...

相关文档:FSScript SQL helper functions

github.com/foggy-proje...

相关推荐
卷心菜投手ovo1 小时前
RAG 为什么引用总是对不上?
后端·github
无风听海2 小时前
ASP.NET Core .NET 10 错误响应体系全景:从 BadRequest 到编译器基础设施
后端·asp.net·.net
文心快码BaiduComate2 小时前
从个人效能到组织资产:文心快码企业版Agent Hub上线,提升团队AI编程效能
前端·后端·程序员
雪隐2 小时前
个人电脑玩AI00-前言
人工智能·后端
我是一颗柠檬3 小时前
【Java后端技术亮点】动态路由权限(按钮级权限),细粒度控制到按钮级别
java·开发语言·后端·状态模式
前端Hardy3 小时前
CSS 动画真的比 JS 快?Josh Comeau 做了组实验,结果跟直觉不一样
前端·javascript·后端
Front思3 小时前
调取支付宝支付正式环境不可以唤起来,但是沙箱可以
后端
foggyprojects3 小时前
AI 生成 SQL 模板以后,为什么还需要固定 helper 规则
后端
明天一点3 小时前
Cloudflare 通知转发钉钉机器人
前端·后端