集成 Spring Boot Admin 监控服务端及客户端依赖,并新增 AdminServerConfig 配置类启用应用监控。

This commit is contained in:
Ray.Hao
2026-03-16 08:09:03 +08:00
parent 2931153422
commit 63c8cbc873
45 changed files with 257 additions and 140 deletions

View File

@@ -0,0 +1,267 @@
package com.youlai.boot.config;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.youlai.boot.common.annotation.DataPermission;
import com.youlai.boot.common.enums.DataScopeEnum;
import com.youlai.boot.security.model.RoleDataScope;
import com.youlai.boot.security.model.SysUserDetails;
import com.youlai.boot.security.util.SecurityUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import java.lang.reflect.Method;
import java.util.List;
/**
* 数据权限控制器
* <p>
* 支持多角色数据权限合并(并集策略):
* - 如果任一角色是 ALL则跳过数据权限过滤
* - 否则用 OR 连接各角色的数据权限条件
* <p>
* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。
*
* @author zc
* @since 2021-12-10 13:28
*/
@Slf4j
public class MyDataPermissionHandler implements DataPermissionHandler {
private static final String DEPT_TABLE = "sys_dept";
private static final String DEPT_ID_COLUMN = "id";
private static final String DEPT_TREE_PATH_COLUMN = "tree_path";
/**
* 获取数据权限的sql片段
*
* @param where 查询条件
* @param mappedStatementId mapper接口方法的全路径
* @return sql片段
*/
@Override
@SneakyThrows
public Expression getSqlSegment(Expression where, String mappedStatementId) {
// 如果是未登录或者是定时任务执行的SQL或者是超级管理员直接返回
if (SecurityUtils.getUserId() == null || SecurityUtils.isRoot()) {
return where;
}
// 获取当前用户的数据权限列表
List<RoleDataScope> dataScopes = SecurityUtils.getUser()
.map(SysUserDetails::getDataScopes)
.orElse(List.of());
// 如果任一角色是 ALL则跳过数据权限过滤并集策略
if (hasAllDataScope(dataScopes)) {
return where;
}
// 如果没有数据权限,跳过过滤
if (CollectionUtil.isEmpty(dataScopes)) {
return where;
}
// 获取当前执行的接口类
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT)));
// 获取当前执行的方法名称
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1);
// 获取当前执行的接口类里所有的方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
// 找到当前执行的方法
if (method.getName().equals(methodName)) {
DataPermission annotation = method.getAnnotation(DataPermission.class);
// 判断当前执行的方法是否有权限注解,如果没有注解直接返回
if (annotation == null) {
return where;
}
// 使用并集策略过滤
return dataScopeFilterWithUnion(mappedStatementId, annotation, dataScopes, where);
}
}
return where;
}
/**
* 判断是否包含"全部数据"权限
*
* @param dataScopes 数据权限列表
* @return 是否有全部数据权限
*/
private boolean hasAllDataScope(List<RoleDataScope> dataScopes) {
if (CollectionUtil.isEmpty(dataScopes)) {
return false;
}
return dataScopes.stream()
.anyMatch(scope -> DataScopeEnum.ALL.getValue().equals(scope.getDataScope()));
}
/**
* 使用并集策略进行数据权限过滤
* <p>
* 多个角色的数据权限通过 OR 连接,实现并集效果
*
* @param annotation 数据权限注解
* @param dataScopes 数据权限列表
* @param where 原始查询条件
* @return 追加权限过滤后的查询条件
*/
@SneakyThrows
private Expression dataScopeFilterWithUnion(String mappedStatementId, DataPermission annotation, List<RoleDataScope> dataScopes, Expression where) {
String deptAlias = annotation.deptAlias();
String deptIdColumnName = annotation.deptIdColumnName();
String userAlias = annotation.userAlias();
String userIdColumnName = annotation.userIdColumnName();
// 构建各角色的数据权限条件,使用 OR 连接实现并集
Expression unionExpression = null;
for (RoleDataScope dataScope : dataScopes) {
Expression roleExpression = buildRoleDataScopeExpression(
deptAlias, deptIdColumnName, userAlias, userIdColumnName, dataScope);
if (roleExpression != null) {
if (unionExpression == null) {
unionExpression = roleExpression;
} else {
// 使用 OR 连接各角色的条件(并集)
unionExpression = new OrExpression(unionExpression, roleExpression);
}
}
}
if (unionExpression == null) {
return where;
}
// 用括号包裹并集条件
Expression finalExpression = CCJSqlParserUtil.parseCondExpression("(" + unionExpression + ")");
if (where == null) {
log.debug("DataPermission applied. mappedStatementId={}, segment={}", mappedStatementId, finalExpression);
return finalExpression;
}
Expression combined = new AndExpression(where, finalExpression);
log.debug("DataPermission applied. mappedStatementId={}, originWhere={}, segment={}, combined={}",
mappedStatementId, where, finalExpression, combined);
return combined;
}
/**
* 构建单个角色的数据权限SQL条件
* <p>
* 使用 JSQLParser 构建 Expression避免字符串拼接
*
* @param deptAlias 部门表别名
* @param deptIdColumnName 部门ID字段名
* @param userAlias 用户表别名
* @param userIdColumnName 用户ID字段名
* @param roleDataScope 角色数据权限
* @return 数据权限条件表达式
*/
private Expression buildRoleDataScopeExpression(String deptAlias, String deptIdColumnName,
String userAlias, String userIdColumnName,
RoleDataScope roleDataScope) {
Column deptColumn = buildColumn(deptAlias, deptIdColumnName);
Column userColumn = buildColumn(userAlias, userIdColumnName);
Long deptId = SecurityUtils.getDeptId();
Long userId = SecurityUtils.getUserId();
DataScopeEnum dataScopeEnum = DataScopeEnum.getByValue(roleDataScope.getDataScope());
if (dataScopeEnum == null) {
return null;
}
return switch (dataScopeEnum) {
case ALL -> null; // 全部数据权限,不添加过滤条件
case DEPT_AND_SUB -> buildDeptAndSubExpression(deptColumn, deptId);
case DEPT -> buildEqualsExpression(deptColumn, deptId);
case SELF -> buildEqualsExpression(userColumn, userId);
case CUSTOM -> buildCustomDeptExpression(deptColumn, roleDataScope.getCustomDeptIds());
};
}
/**
* 构建列引用
*
* @param alias 表别名
* @param columnName 列名
* @return 列引用
*/
private Column buildColumn(String alias, String columnName) {
if (StrUtil.isNotBlank(alias)) {
return new Column(alias + StringPool.DOT + columnName);
}
return new Column(columnName);
}
/**
* 构建等于条件
*
* @param column 列
* @param value 值
* @return 等于表达式
*/
private Expression buildEqualsExpression(Column column, Long value) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(column);
equalsTo.setRightExpression(new LongValue(value));
return equalsTo;
}
/**
* 构建部门及子部门数据权限条件
* <p>
* SQL: dept_id IN (SELECT id FROM sys_dept WHERE id = ? OR FIND_IN_SET(?, tree_path))
*
* @param deptColumn 部门列
* @param deptId 部门ID
* @return IN 子查询表达式
*/
@SneakyThrows
private Expression buildDeptAndSubExpression(Column deptColumn, Long deptId) {
// 使用字符串解析,避免不同 JSqlParser 版本下 InExpression/ItemsList 渲染差异导致 SQL 语法错误
// SQL: dept_id IN (SELECT id FROM sys_dept WHERE id = ? OR FIND_IN_SET(?, tree_path))
String columnName = deptColumn.toString();
String sql = columnName + " IN (SELECT " + DEPT_ID_COLUMN + " FROM " + DEPT_TABLE +
" WHERE " + DEPT_ID_COLUMN + " = " + deptId +
" OR FIND_IN_SET(" + deptId + ", " + DEPT_TREE_PATH_COLUMN + "))";
return CCJSqlParserUtil.parseCondExpression(sql);
}
/**
* 构建自定义部门数据权限条件
* <p>
* SQL: dept_id IN (?, ?, ...)
*
* @param deptColumn 部门列
* @param customDeptIds 自定义部门ID列表
* @return IN 表达式,如果没有部门则返回 1=0
*/
@SneakyThrows
private Expression buildCustomDeptExpression(Column deptColumn, List<Long> customDeptIds) {
if (CollectionUtil.isEmpty(customDeptIds)) {
// 没有自定义部门,返回 1=0无权限
EqualsTo falseCondition = new EqualsTo();
falseCondition.setLeftExpression(new LongValue(1));
falseCondition.setRightExpression(new LongValue(0));
return falseCondition;
}
// 使用字符串解析,确保渲染始终为 IN (..)
String columnName = deptColumn.toString();
String ids = customDeptIds.stream().map(String::valueOf).reduce((a, b) -> a + "," + b).orElse("");
String sql = columnName + " IN (" + ids + ")";
return CCJSqlParserUtil.parseCondExpression(sql);
}
}