Skip to content

Java 编程式权限控制

除了在 QM 文件中声明式定义权限,Foggy Dataset Model 还支持通过 Java 代码动态控制权限。这种方式更灵活,适合复杂的业务场景。

1. 核心架构

1.1 执行流程

在查询执行过程中,DataSetResultFilterManager 会调用所有注册的 DataSetResultStep

java
// 查询前预处理
dataSetResultFilterManager.beforeQuery(context);

// 执行查询
PagingResultImpl result = jdbcService.query(context.getRequest());

// 查询后处理
dataSetResultFilterManager.process(context);

1.2 核心接口

DataSetResultStep - 预处理步骤接口:

java
public interface DataSetResultStep extends FoggyStep<ModelResultContext> {

    /**
     * 查询前处理
     * - 添加权限过滤条件(修改 slice)
     * - 控制可见字段(修改 columns/exColumns)
     * - 验证请求合法性
     */
    default int beforeQuery(ModelResultContext ctx) {
        return CONTINUE;
    }

    /**
     * 查询后处理
     * - 数据脱敏
     * - 格式转换
     * - 结果集过滤
     */
    default int process(ModelResultContext ctx) {
        return CONTINUE;
    }
}

返回值说明

  • CONTINUE (0) - 继续执行下一个步骤
  • ABORT (1) - 中止执行链(非错误情况)

2. 行级权限控制

2.1 基本示例

通过向 queryRequest.param.slice 添加过滤条件来控制行级权限:

java
import com.foggyframework.dataset.db.model.plugins.result_set_filter.DataSetResultStep;
import com.foggyframework.dataset.db.model.plugins.result_set_filter.ModelResultContext;
import com.foggyframework.dataset.db.model.def.query.request.SliceRequestDef;
import org.springframework.stereotype.Component;

@Component
public class TenantAuthorizationStep implements DataSetResultStep {

    @Override
    public int beforeQuery(ModelResultContext ctx) {
        // 1. 获取当前用户的租户信息
        String tenantId = getCurrentUserTenantId();

        // 2. 创建租户过滤条件
        SliceRequestDef tenantFilter = new SliceRequestDef();
        tenantFilter.setField("tenantId");
        tenantFilter.setOp("eq");  // 等于
        tenantFilter.setValue(tenantId);

        // 3. 添加到现有过滤条件
        List<SliceRequestDef> slice = ctx.getRequest().getParam().getSlice();
        if (slice == null) {
            slice = new ArrayList<>();
            ctx.getRequest().getParam().setSlice(slice);
        }
        slice.add(tenantFilter);

        return CONTINUE;
    }

    private String getCurrentUserTenantId() {
        // 从 SecurityContext 或 ThreadLocal 获取当前用户租户ID
        return SecurityContextHolder.getContext().getTenantId();
    }

    @Override
    public int order() {
        return 1000;  // 权限过滤应优先执行
    }
}

2.2 使用 SecurityContext

ModelResultContext 内置了 SecurityContext 用于传递认证信息:

java
@Component
public class RoleBasedAuthorizationStep implements DataSetResultStep {

    @Override
    public int beforeQuery(ModelResultContext ctx) {
        // 1. 获取安全上下文
        ModelResultContext.SecurityContext security = ctx.getSecurityContext();
        if (security == null) {
            throw new UnauthorizedException("未登录");
        }

        // 2. 根据角色添加不同的过滤条件
        String userId = security.getUserId();
        List<String> roles = security.getRoles();

        if (roles.contains("ADMIN")) {
            // 管理员无限制,不添加过滤条件
            return CONTINUE;
        }

        if (roles.contains("MANAGER")) {
            // 经理可查看本部门数据
            addFilter(ctx, "deptId", security.getDeptId());
        } else {
            // 普通用户只能查看自己的数据
            addFilter(ctx, "userId", userId);
        }

        return CONTINUE;
    }

    private void addFilter(ModelResultContext ctx, String field, String value) {
        SliceRequestDef filter = new SliceRequestDef();
        filter.setField(field);
        filter.setOp("eq");
        filter.setValue(value);

        List<SliceRequestDef> slice = ctx.getRequest().getParam().getSlice();
        if (slice == null) {
            slice = new ArrayList<>();
            ctx.getRequest().getParam().setSlice(slice);
        }
        slice.add(filter);
    }

    @Override
    public int order() {
        return 1000;
    }
}

2.3 复杂过滤条件

支持所有 DSL 操作符

java
@Component
public class ComplexAuthorizationStep implements DataSetResultStep {

    @Override
    public int beforeQuery(ModelResultContext ctx) {
        String userId = getCurrentUserId();
        List<SliceRequestDef> slice = getOrCreateSlice(ctx);

        // 示例 1: IN 条件 - 只能查看指定区域的数据
        SliceRequestDef regionFilter = new SliceRequestDef();
        regionFilter.setField("regionId");
        regionFilter.setOp("in");
        regionFilter.setValue(Arrays.asList("R001", "R002", "R003"));
        slice.add(regionFilter);

        // 示例 2: 范围条件 - 只能查看最近 30 天的数据
        SliceRequestDef dateFilter = new SliceRequestDef();
        dateFilter.setField("orderDate");
        dateFilter.setOp(">=");
        dateFilter.setValue(LocalDate.now().minusDays(30));
        slice.add(dateFilter);

        // 示例 3: OR 条件 - 可以查看自己创建的或指派给自己的订单
        CondRequestDef creatorCond = new CondRequestDef();
        creatorCond.setField("creatorId");
        creatorCond.setOp("eq");
        creatorCond.setValue(userId);

        CondRequestDef assigneeCond = new CondRequestDef();
        assigneeCond.setField("assigneeId");
        assigneeCond.setOp("eq");
        assigneeCond.setValue(userId);

        // 使用 $or 语法创建 OR 条件组
        SliceRequestDef orFilter = SliceRequestDef.or(Arrays.asList(creatorCond, assigneeCond));
        slice.add(orFilter);

        return CONTINUE;
    }

    private List<SliceRequestDef> getOrCreateSlice(ModelResultContext ctx) {
        List<SliceRequestDef> slice = ctx.getRequest().getParam().getSlice();
        if (slice == null) {
            slice = new ArrayList<>();
            ctx.getRequest().getParam().setSlice(slice);
        }
        return slice;
    }

    @Override
    public int order() {
        return 1000;
    }
}

3. 列级权限控制

3.1 控制可见字段

通过修改 columnsexColumns 来控制字段级权限:

java
@Component
public class ColumnAuthorizationStep implements DataSetResultStep {

    @Override
    public int beforeQuery(ModelResultContext ctx) {
        List<String> roles = getCurrentUserRoles();
        DbQueryRequestDef param = ctx.getRequest().getParam();

        // 非财务人员不能查看金额字段
        if (!roles.contains("FINANCE")) {
            List<String> exColumns = param.getExColumns();
            if (exColumns == null) {
                exColumns = new ArrayList<>();
                param.setExColumns(exColumns);
            }

            // 添加要排除的字段
            exColumns.add("costAmount");
            exColumns.add("profitAmount");
            exColumns.add("taxAmount");
        }

        // 非管理员不能查看敏感字段
        if (!roles.contains("ADMIN")) {
            List<String> exColumns = param.getExColumns();
            if (exColumns == null) {
                exColumns = new ArrayList<>();
                param.setExColumns(exColumns);
            }

            exColumns.add("customer$phone");
            exColumns.add("customer$idCard");
        }

        return CONTINUE;
    }

    @Override
    public int order() {
        return 1000;
    }
}

3.2 动态调整查询字段

java
@Component
public class DynamicColumnStep implements DataSetResultStep {

    @Override
    public int beforeQuery(ModelResultContext ctx) {
        DbQueryRequestDef param = ctx.getRequest().getParam();
        List<String> columns = param.getColumns();

        // 如果未指定 columns,根据权限设置默认字段
        if (columns == null || columns.isEmpty()) {
            columns = getDefaultColumnsForUser();
            param.setColumns(columns);
        } else {
            // 过滤掉用户没有权限的字段
            columns = columns.stream()
                    .filter(this::hasPermission)
                    .collect(Collectors.toList());
            param.setColumns(columns);
        }

        return CONTINUE;
    }

    private List<String> getDefaultColumnsForUser() {
        List<String> roles = getCurrentUserRoles();

        if (roles.contains("ADMIN")) {
            return Arrays.asList(
                "orderId", "customer$caption", "salesAmount",
                "customer$phone", "customer$idCard"
            );
        } else if (roles.contains("SALES")) {
            return Arrays.asList(
                "orderId", "customer$caption", "salesAmount"
            );
        } else {
            return Arrays.asList(
                "orderId", "customer$caption"
            );
        }
    }

    private boolean hasPermission(String column) {
        // 检查用户是否有权限查看该字段
        // 可以从配置文件或数据库读取字段权限配置
        return true;  // 示例:允许所有字段
    }

    @Override
    public int order() {
        return 1000;
    }
}

4. 查询后数据处理

4.1 数据脱敏

process() 方法中处理查询结果:

java
@Component
public class DataMaskingStep implements DataSetResultStep {

    @Override
    public int process(ModelResultContext ctx) {
        PagingResultImpl result = ctx.getPagingResult();
        if (result == null || result.getData() == null) {
            return CONTINUE;
        }

        List<String> roles = getCurrentUserRoles();

        // 非管理员需要脱敏手机号和身份证
        if (!roles.contains("ADMIN")) {
            for (Map<String, Object> row : result.getData()) {
                maskPhoneNumber(row, "customer$phone");
                maskIdCard(row, "customer$idCard");
            }
        }

        return CONTINUE;
    }

    private void maskPhoneNumber(Map<String, Object> row, String key) {
        Object value = row.get(key);
        if (value instanceof String) {
            String phone = (String) value;
            if (phone.length() == 11) {
                row.put(key, phone.substring(0, 3) + "****" + phone.substring(7));
            }
        }
    }

    private void maskIdCard(Map<String, Object> row, String key) {
        Object value = row.get(key);
        if (value instanceof String) {
            String idCard = (String) value;
            if (idCard.length() == 18) {
                row.put(key, idCard.substring(0, 6) + "********" + idCard.substring(14));
            }
        }
    }

    @Override
    public int order() {
        return 5000;  // 数据处理步骤可以晚一些执行
    }
}

4.2 金额单位转换

java
@Component
public class MoneyConversionStep implements DataSetResultStep {

    @Override
    public int process(ModelResultContext ctx) {
        PagingResultImpl result = ctx.getPagingResult();
        if (result == null || result.getData() == null) {
            return CONTINUE;
        }

        // 金额字段列表(从配置或元数据获取)
        List<String> moneyFields = Arrays.asList(
            "salesAmount", "costAmount", "profitAmount"
        );

        // 将分转换为元
        for (Map<String, Object> row : result.getData()) {
            for (String field : moneyFields) {
                Object value = row.get(field);
                if (value instanceof Long) {
                    row.put(field, ((Long) value) / 100.0);
                } else if (value instanceof Integer) {
                    row.put(field, ((Integer) value) / 100.0);
                }
            }
        }

        return CONTINUE;
    }

    @Override
    public int order() {
        return 5000;
    }
}

5. 注册和配置

5.1 自动注册

将 Step 声明为 Spring Bean 即可自动注册:

java
@Component
public class MyAuthorizationStep implements DataSetResultStep {
    // ...
}

5.2 手动注册

通过 Spring 配置类注册:

java
@Configuration
public class DatasetAuthConfig {

    @Bean
    public DataSetResultStep tenantAuthStep() {
        TenantAuthorizationStep step = new TenantAuthorizationStep();
        step.setTenantIdColumn("tenantId");
        return step;
    }

    @Bean
    public DataSetResultStep columnAuthStep() {
        return new ColumnAuthorizationStep();
    }

    @Bean
    public DataSetResultStep dataMaskingStep() {
        return new DataMaskingStep();
    }
}

5.3 执行顺序

通过 order() 方法控制执行顺序(数字越小越先执行):

java
@Override
public int order() {
    return 1000;  // 推荐值:权限过滤 1000,数据处理 5000
}

系统内置步骤的顺序

  • QueryRequestValidationStep - 0(参数校验)
  • AuthorizationStep - 1000(权限过滤)
  • InlineExpressionPreprocessStep - 2000(表达式预处理)
  • AutoGroupByStep - 3000(自动分组)
  • SemanticMoneyStep - 5000(金额转换)

6. 完整示例

6.1 多租户 + 角色权限

java
@Component
@Slf4j
public class MultiTenantAuthStep implements DataSetResultStep {

    @Resource
    private UserService userService;

    @Resource
    private TenantService tenantService;

    @Override
    public int beforeQuery(ModelResultContext ctx) {
        // 1. 获取安全上下文
        ModelResultContext.SecurityContext security = ctx.getSecurityContext();
        if (security == null) {
            throw new UnauthorizedException("用户未登录");
        }

        // 2. 解析 JWT token(如果需要)
        parseJwtToken(security);

        // 3. 获取用户信息
        String userId = security.getUserId();
        String tenantId = security.getTenantId();
        List<String> roles = security.getRoles();

        log.debug("用户 {} (租户 {}, 角色 {}) 发起查询", userId, tenantId, roles);

        // 4. 添加租户过滤(所有用户都必须)
        addTenantFilter(ctx, tenantId);

        // 5. 根据角色添加行权限
        if (!roles.contains("ADMIN")) {
            if (roles.contains("MANAGER")) {
                addDepartmentFilter(ctx, security.getDeptId());
            } else {
                addUserFilter(ctx, userId);
            }
        }

        // 6. 控制列权限
        if (!roles.contains("FINANCE")) {
            hideFinanceColumns(ctx);
        }

        if (!roles.contains("ADMIN")) {
            hideSensitiveColumns(ctx);
        }

        return CONTINUE;
    }

    @Override
    public int process(ModelResultContext ctx) {
        // 查询后数据脱敏
        List<String> roles = getCurrentUserRoles();
        if (!roles.contains("ADMIN")) {
            maskSensitiveData(ctx.getPagingResult());
        }

        return CONTINUE;
    }

    private void parseJwtToken(ModelResultContext.SecurityContext security) {
        String authorization = security.getAuthorization();
        if (authorization != null && authorization.startsWith("Bearer ")) {
            String token = authorization.substring(7);
            // 解析 JWT,填充 userId, tenantId, roles 等
            JwtClaims claims = JwtUtils.parse(token);
            security.setUserId(claims.getUserId());
            security.setTenantId(claims.getTenantId());
            security.setRoles(claims.getRoles());
            security.setDeptId(claims.getDeptId());
        }
    }

    private void addTenantFilter(ModelResultContext ctx, String tenantId) {
        SliceRequestDef filter = new SliceRequestDef();
        filter.setField("tenantId");
        filter.setOp("eq");
        filter.setValue(tenantId);
        getOrCreateSlice(ctx).add(filter);
    }

    private void addDepartmentFilter(ModelResultContext ctx, String deptId) {
        SliceRequestDef filter = new SliceRequestDef();
        filter.setField("deptId");
        filter.setOp("eq");
        filter.setValue(deptId);
        getOrCreateSlice(ctx).add(filter);
    }

    private void addUserFilter(ModelResultContext ctx, String userId) {
        SliceRequestDef filter = new SliceRequestDef();
        filter.setField("userId");
        filter.setOp("eq");
        filter.setValue(userId);
        getOrCreateSlice(ctx).add(filter);
    }

    private void hideFinanceColumns(ModelResultContext ctx) {
        List<String> exColumns = getOrCreateExColumns(ctx);
        exColumns.addAll(Arrays.asList(
            "salesAmount", "costAmount", "profitAmount", "taxAmount"
        ));
    }

    private void hideSensitiveColumns(ModelResultContext ctx) {
        List<String> exColumns = getOrCreateExColumns(ctx);
        exColumns.addAll(Arrays.asList(
            "customer$phone", "customer$idCard", "customer$phone"
        ));
    }

    private void maskSensitiveData(PagingResultImpl result) {
        if (result == null || result.getData() == null) {
            return;
        }

        for (Map<String, Object> row : result.getData()) {
            maskPhoneNumber(row, "customer$phone");
            maskIdCard(row, "customer$idCard");
        }
    }

    private List<SliceRequestDef> getOrCreateSlice(ModelResultContext ctx) {
        List<SliceRequestDef> slice = ctx.getRequest().getParam().getSlice();
        if (slice == null) {
            slice = new ArrayList<>();
            ctx.getRequest().getParam().setSlice(slice);
        }
        return slice;
    }

    private List<String> getOrCreateExColumns(ModelResultContext ctx) {
        DbQueryRequestDef param = ctx.getRequest().getParam();
        List<String> exColumns = param.getExColumns();
        if (exColumns == null) {
            exColumns = new ArrayList<>();
            param.setExColumns(exColumns);
        }
        return exColumns;
    }

    @Override
    public int order() {
        return 1000;
    }
}

7. SecurityContext 设置

7.1 在 Controller 中设置

java
@RestController
@RequestMapping("/api/dataset")
public class DatasetController {

    @Resource
    private QueryFacade queryFacade;

    @PostMapping("/query")
    public PagingResultImpl query(
            @RequestBody PagingRequest<DbQueryRequestDef> request,
            @RequestHeader(value = "Authorization", required = false) String authorization
    ) {
        // 1. 创建 SecurityContext
        ModelResultContext.SecurityContext security =
            ModelResultContext.SecurityContext.builder()
                .authorization(authorization)
                .userId(getCurrentUserId())
                .tenantId(getCurrentTenantId())
                .roles(getCurrentUserRoles())
                .deptId(getCurrentDeptId())
                .build();

        // 2. 设置到请求扩展数据中(供 Step 使用)
        request.setSecurityContext(security);

        // 3. 执行查询
        return queryFacade.query(request);
    }
}

7.2 在 Filter 中设置

java
@Component
public class SecurityContextFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        String authorization = request.getHeader("Authorization");
        if (authorization != null) {
            // 解析 JWT 或 Session,设置到 ThreadLocal
            SecurityContextHolder.setContext(
                parseAuthorizationHeader(authorization)
            );
        }

        try {
            filterChain.doFilter(request, response);
        } finally {
            SecurityContextHolder.clear();
        }
    }
}

8. 与 QM 权限的对比

对比维度QM 文件权限(声明式)Java Step 权限(编程式)
定义位置.qm 文件中的 accessesJava 代码中的 DataSetResultStep
执行时机查询构建时(SQL 生成阶段)查询请求预处理阶段
灵活性较低,适合固定规则高,适合复杂动态逻辑
权限粒度行级(基于字段或维度)行级 + 列级 + 结果集
上下文获取通过 import 从 Spring Bean 获取直接访问 SecurityContext
数据脱敏不支持支持(在 process() 中处理)
参数验证不支持支持(在 beforeQuery() 中验证)
适用场景简单、固定的权限规则复杂、动态的业务逻辑

推荐使用策略

  • 简单权限(如按团队、部门过滤)→ 使用 QM 文件
  • 复杂权限(如多租户 + 角色 + 列权限)→ 使用 Java Step
  • 数据脱敏、格式转换 → 必须使用 Java Step

9. 注意事项

9.1 安全性

  • SQL 注入防护:使用 SliceRequestDef 设置过滤条件,系统会自动参数化
  • 避免直接拼接 SQL:不要手动构造 SQL 字符串
  • 验证用户身份:在 beforeQuery() 中必须验证 SecurityContext

9.2 性能

  • 索引优化:确保权限过滤字段(如 tenant_iddept_id)有索引
  • 避免重复查询:不要在 Step 中执行额外的数据库查询
  • 缓存用户信息:从 JWT token 解析的用户信息应缓存到 SecurityContext

9.3 调试

开启日志查看执行过程:

yaml
logging:
  level:
    com.foggyframework.dataset.db.model.plugins: DEBUG

下一步