This commit is contained in:
kk
2026-07-07 18:01:21 +08:00
commit b259f94d4b
1088 changed files with 121778 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>BladeX-Tool</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<artifactId>blade-starter-tenant</artifactId>
<name>${project.artifactId}</name>
<version>${project.parent.version}</version>
<packaging>jar</packaging>
<properties>
<module.name>org.springblade.blade.starter.tenant</module.name>
</properties>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-context</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-cache</artifactId>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<scope>provided</scope>
</dependency>
<!--Dynamic-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<scope>provided</scope>
</dependency>
<!-- Auto -->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,139 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import org.springblade.core.tenant.annotation.TableExclude;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* 租户信息处理器
*
* @author Chill, L.cm
*/
@Slf4j
@RequiredArgsConstructor
public class BladeTenantHandler implements TenantLineHandler, SmartInitializingSingleton {
/**
* 匹配的多租户表
*/
private final List<String> tenantTableList = new ArrayList<>();
/**
* 需要排除进行自定义的多租户表
*/
private final List<String> excludeTableList = Arrays.asList("blade_user", "blade_dept", "blade_role", "blade_tenant", "act_de_model");
/**
* 多租户配置
*/
private final BladeTenantProperties tenantProperties;
/**
* 获取租户ID
*
* @return 租户ID
*/
@Override
public Expression getTenantId() {
return new StringValue(Func.toStr(TenantUtil.getTenantId(), BladeConstant.ADMIN_TENANT_ID));
}
/**
* 获取租户字段名称
*
* @return 租户字段名称
*/
@Override
public String getTenantIdColumn() {
return tenantProperties.getColumn();
}
/**
* 根据表名判断是否忽略拼接多租户条件
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
if (BladeTenantHolder.isIgnore()) {
return true;
}
if (TenantUtil.isIgnore()) {
return true;
}
return !(tenantTableList.contains(tableName) && StringUtil.isNotBlank(TenantUtil.getTenantId()));
}
@Override
public void afterSingletonsInstantiated() {
ApplicationContext context = SpringUtil.getContext();
if (tenantProperties.getAnnotationExclude() && context != null) {
Map<String, Object> tables = context.getBeansWithAnnotation(TableExclude.class);
List<String> excludeTables = tenantProperties.getExcludeTables();
for (Object o : tables.values()) {
TableExclude annotation = o.getClass().getAnnotation(TableExclude.class);
String value = annotation.value();
excludeTables.add(value);
}
}
List<TableInfo> tableInfos = TableInfoHelper.getTableInfos();
tableFor:
for (TableInfo tableInfo : tableInfos) {
String tableName = tableInfo.getTableName();
if (tenantProperties.getExcludeTables().contains(tableName) ||
excludeTableList.contains(tableName.toLowerCase()) ||
excludeTableList.contains(tableName.toUpperCase())) {
continue;
}
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
for (TableFieldInfo fieldInfo : fieldList) {
String column = fieldInfo.getColumn();
if (tenantProperties.getColumn().equals(column)) {
tenantTableList.add(tableName);
continue tableFor;
}
}
}
}
}
@@ -0,0 +1,58 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant;
import org.springframework.core.NamedThreadLocal;
/**
* 租户线程处理
*
* @author Chill
*/
public class BladeTenantHolder {
private static final ThreadLocal<Boolean> TENANT_KEY_HOLDER = new NamedThreadLocal<Boolean>("blade-tenant") {
@Override
protected Boolean initialValue() {
return Boolean.FALSE;
}
};
public static void setIgnore(Boolean ignore) {
TENANT_KEY_HOLDER.set(ignore);
}
public static Boolean isIgnore() {
return TENANT_KEY_HOLDER.get();
}
public static void clear() {
TENANT_KEY_HOLDER.remove();
}
}
@@ -0,0 +1,41 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant;
import org.springblade.core.tool.utils.RandomType;
import org.springblade.core.tool.utils.StringUtil;
/**
* blade租户id生成器
*
* @author Chill
*/
public class BladeTenantId implements TenantId {
@Override
public String generate() {
return StringUtil.random(6, RandomType.INT);
}
}
@@ -0,0 +1,193 @@
/*
* Copyright (c) 2011-2024, baomidou (jobob@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.tenant;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.ParenthesedFromItem;
import net.sf.jsqlparser.statement.select.PlainSelect;
import java.util.*;
/**
* 租户基础拦截器,拓展mybatis-plus私有方法便于继承
*
* @author Chill、hubin
*/
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class BladeTenantInnerInterceptor extends TenantLineInnerInterceptor {
@Override
public void setTenantLineHandler(TenantLineHandler tenantLineHandler) {
super.setTenantLineHandler(tenantLineHandler);
}
/**
* 获取主表列表
*/
protected List<Table> processMainTables(final PlainSelect plainSelect, final String whereSegment) {
FromItem fromItem = plainSelect.getFromItem();
List<Table> list = processFromItem(fromItem, whereSegment);
return new ArrayList<>(list);
}
/**
* 将父类方法修改为 protected,便于子类继承
*/
protected List<Table> processFromItem(FromItem fromItem, final String whereSegment) {
// 处理括号括起来的表达式
List<Table> mainTables = new ArrayList<>();
// 无 join 时的处理逻辑
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
mainTables.add(fromTable);
} else if (fromItem instanceof ParenthesedFromItem) {
// SubJoin 类型则还需要添加上 where 条件
List<Table> tables = processSubJoin((ParenthesedFromItem) fromItem, whereSegment);
mainTables.addAll(tables);
} else {
// 处理下 fromItem
processOtherFromItem(fromItem, whereSegment);
}
return mainTables;
}
/**
* 将父类方法修改为 protected,便于子类继承
*/
protected List<Table> processSubJoin(ParenthesedFromItem subJoin, final String whereSegment) {
while (subJoin.getJoins() == null && subJoin.getFromItem() instanceof ParenthesedFromItem) {
subJoin = (ParenthesedFromItem) subJoin.getFromItem();
}
List<Table> tableList = processFromItem(subJoin.getFromItem(), whereSegment);
List<Table> mainTables = new ArrayList<>(tableList);
if (subJoin.getJoins() != null) {
processJoins(mainTables, subJoin.getJoins(), whereSegment);
}
return mainTables;
}
/**
* 将父类方法修改为 protected,便于子类继承
*/
protected List<Table> processJoins(List<Table> mainTables, List<Join> joins, final String whereSegment) {
// join 表达式中最终的主表
Table mainTable = null;
// 当前 join 的左表
Table leftTable = null;
if (mainTables.size() == 1) {
mainTable = mainTables.get(0);
leftTable = mainTable;
}
//对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
Deque<List<Table>> onTableDeque = new LinkedList<>();
for (Join join : joins) {
// 处理 on 表达式
FromItem joinItem = join.getRightItem();
// 获取当前 join 的表,subJoint 可以看作是一张表
List<Table> joinTables = null;
if (joinItem instanceof Table) {
joinTables = new ArrayList<>();
joinTables.add((Table) joinItem);
} else if (joinItem instanceof ParenthesedFromItem) {
joinTables = processSubJoin((ParenthesedFromItem) joinItem, whereSegment);
}
if (joinTables != null && !joinTables.isEmpty()) {
// 如果是隐式内连接
if (join.isSimple()) {
mainTables.addAll(joinTables);
continue;
}
// 当前表是否忽略
Table joinTable = joinTables.get(0);
List<Table> onTables = null;
// 如果不要忽略,且是右连接,则记录下当前表
if (join.isRight()) {
mainTable = joinTable;
mainTables.clear();
if (leftTable != null) {
onTables = Collections.singletonList(leftTable);
}
} else if (join.isInner()) {
if (mainTable == null) {
onTables = Collections.singletonList(joinTable);
} else {
onTables = Arrays.asList(mainTable, joinTable);
}
mainTable = null;
mainTables.clear();
} else {
onTables = Collections.singletonList(joinTable);
}
if (mainTable != null && !mainTables.contains(mainTable)) {
mainTables.add(mainTable);
}
// 获取 join 尾缀的 on 表达式列表
Collection<Expression> originOnExpressions = join.getOnExpressions();
// 正常 join on 表达式只有一个,立刻处理
if (originOnExpressions.size() == 1 && onTables != null) {
List<Expression> onExpressions = new LinkedList<>();
onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables, whereSegment));
join.setOnExpressions(onExpressions);
leftTable = mainTable == null ? joinTable : mainTable;
continue;
}
// 表名压栈,忽略的表压入 null,以便后续不处理
onTableDeque.push(onTables);
// 尾缀多个 on 表达式的时候统一处理
if (originOnExpressions.size() > 1) {
Collection<Expression> onExpressions = new LinkedList<>();
for (Expression originOnExpression : originOnExpressions) {
List<Table> currentTableList = onTableDeque.poll();
if (CollectionUtils.isEmpty(currentTableList)) {
onExpressions.add(originOnExpression);
} else {
onExpressions.add(builderExpression(originOnExpression, currentTableList, whereSegment));
}
}
join.setOnExpressions(onExpressions);
}
leftTable = joinTable;
} else {
processOtherFromItem(joinItem, whereSegment);
leftTable = null;
}
}
return mainTables;
}
}
@@ -0,0 +1,135 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.update.Update;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.CollectionUtil;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 租户拦截器
*
* @author Chill
*/
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class BladeTenantInterceptor extends BladeTenantInnerInterceptor {
/**
* 租户配置文件
*/
private BladeTenantProperties tenantProperties;
/**
* 超管需要启用租户过滤的表
*/
private List<String> adminTenantTables = Arrays.asList("blade_top_menu", "blade_dict_biz");
@Override
public void setTenantLineHandler(TenantLineHandler tenantLineHandler) {
super.setTenantLineHandler(tenantLineHandler);
}
/**
* select 条件处理
*/
@Override
protected void processPlainSelect(final PlainSelect plainSelect, final String whereSegment) {
// 判断 mainTable 进行条件追加
List<Table> mainTables = super.processMainTables(plainSelect, whereSegment);
if (CollectionUtils.isNotEmpty(mainTables) && !doTenantFilters(mainTables)) {
super.processPlainSelect(plainSelect, whereSegment);
}
}
/**
* insert 条件处理
*/
@Override
protected void processInsert(Insert insert, int index, String sql, Object obj) {
// 官方已支持租户ID自定义设置,无需再定义租户增强功能
super.processInsert(insert, index, sql, obj);
}
/**
* update 条件处理
*/
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
final Table table = update.getTable();
if (doTenantFilter(table.getName())) {
// 过滤退出执行
return;
}
super.processUpdate(update, index, sql, obj);
}
/**
* delete 条件处理
*/
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
final Table table = delete.getTable();
if (doTenantFilter(table.getName())) {
// 过滤退出执行
return;
}
super.processDelete(delete, index, sql, obj);
}
/**
* 判断当前操作是否需要进行过滤
*
* @param tableName 表名
*/
public boolean doTenantFilter(String tableName) {
return AuthUtil.isAdministrator() && tenantProperties.getEnhance() && !adminTenantTables.contains(tableName);
}
/**
* 判断当前操作是否需要进行过滤
*
* @param tables 表名
*/
public boolean doTenantFilters(List<Table> tables) {
List<String> tableNames = tables.stream().map(Table::getName).collect(Collectors.toList());
return AuthUtil.isAdministrator() && tenantProperties.getEnhance() && !CollectionUtil.containsAny(adminTenantTables, tableNames);
}
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* 多租户配置
*
* @author Chill
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "blade.tenant")
public class BladeTenantProperties {
/**
* 是否增强多租户(超管账号关闭租户过滤)
*/
private Boolean enhance = Boolean.TRUE;
/**
* 是否开启授权码校验
*/
private Boolean license = Boolean.FALSE;
/**
* 是否开启动态数据源功能
*/
private Boolean dynamicDatasource = Boolean.FALSE;
/**
* 是否开启动态数据源全局扫描
*/
private Boolean dynamicGlobal = Boolean.FALSE;
/**
* 多租户字段名称
*/
private String column = "tenant_id";
/**
* 是否开启注解排除
*/
private Boolean annotationExclude = Boolean.FALSE;
/**
* 需要排除进行自定义的多租户表
*/
private List<String> excludeTables = new ArrayList<>();
}
@@ -0,0 +1,209 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tenant.exception.TenantException;
import org.springblade.core.tool.utils.CollectionUtil;
import org.springblade.core.tool.utils.ReflectUtil;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 租户归属守卫
* <p>
* 统一拦截「目标实体是否属于当前会话租户」的越权写操作。平台超管(administrator)默认放行。
* 通过反射访问实体的 getId / getTenantId / setTenantId 方法,调用方无需传入 lambda。
*
* @author Chill
*/
public class TenantGuard {
private static final String METHOD_GET_ID = "getId";
private static final String METHOD_GET_TENANT_ID = "getTenantId";
private static final String METHOD_SET_TENANT_ID = "setTenantId";
private static final ConcurrentHashMap<Class<?>, Method> ID_GETTER_CACHE = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<Class<?>, Method> TENANT_GETTER_CACHE = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<Class<?>, Method> TENANT_SETTER_CACHE = new ConcurrentHashMap<>();
private TenantGuard() {
}
/**
* 受租户隔离保护的实体类型
* <p>
* 集中管理实体中文标签,避免调用处散落硬编码字符串。新增受保护实体时,
* 在此枚举追加常量即可,调用方使用 static import 让代码更紧凑。
*/
public enum EntityType {
ROLE("角色"),
ROLE_PARENT("上级角色"),
DEPT("部门"),
DEPT_PARENT("上级部门"),
POST("岗位"),
USER("用户"),
;
private final String label;
EntityType(String label) {
this.label = label;
}
public String label() {
return label;
}
}
/**
* 校验单个 id 归属当前会话租户
* <p>
* 适用于 update/detail by id 等场景。校验通过后返回查询出的实体,
* 调用方可基于返回值进行后续业务(如把 existEntity.tenantId 回写到入参以防篡改)。
*
* @param service MyBatis-Plus IService 实例
* @param id 目标主键
* @param entityType 实体类型枚举
* @param <T> 实体类型
* @return 查询出的实体(仅当超管查询不存在的 id 时可能为 null)
* @throws TenantException 当非超管且目标实体不存在或归属其他租户时抛出
*/
public static <T> T verify(IService<T> service, Long id, EntityType entityType) {
T entity = service.getById(id);
if (AuthUtil.isAdministrator()) {
return entity;
}
if (entity == null || !AuthUtil.getTenantId().equals(tenantIdOf(entity))) {
throw new TenantException("无权操作非本租户的" + entityType.label());
}
return entity;
}
/**
* 提交时(新增 / 修改)的租户绑定守卫
* <p>
* 新增(id 为空):超管放行,非超管强制写入当前会话 tenantId,避免前端注入。<br/>
* 修改(id 不空):调用 {@link #verify} 校验归属,并把已存在实体的 tenantId 回写到入参,防止 update 篡改。
*
* @param service MyBatis-Plus IService 实例
* @param entity 入参实体
* @param entityType 实体类型
* @param <T> 实体类型
* @throws TenantException 当修改路径下目标实体不存在或归属其他租户时抛出
*/
public static <T> void bindTenant(IService<T> service, T entity, EntityType entityType) {
Long id = idOf(entity);
if (id == null) {
if (!AuthUtil.isAdministrator()) {
bindTenantId(entity, AuthUtil.getTenantId());
}
return;
}
T existEntity = verify(service, id, entityType);
if (existEntity == null) {
return;
}
// 非超管路径下 verify 已确保 existEntity.tenantId 等于会话 tenantId,直接复用避免重复反射
String tenantId = AuthUtil.isAdministrator() ? tenantIdOf(existEntity) : AuthUtil.getTenantId();
bindTenantId(entity, tenantId);
}
/**
* 校验批量 ids 全部归属当前会话租户,并返回查询出的实体列表
* <p>
* 适用于 batch remove/grant/reset/unlock 等场景。任一 id 不存在或归属其他租户,整体抛异常。
* 超管放行。返回值供调用方复用,避免二次查询。
*
* @param service MyBatis-Plus IService 实例
* @param ids 目标主键集合
* @param entityType 实体类型枚举
* @param <T> 实体类型
* @return 查询出的实体列表(ids 为空时返回空列表)
* @throws TenantException 当非超管且任一目标实体不存在或归属其他租户时抛出
*/
public static <T> List<T> verifyBatch(IService<T> service, List<Long> ids, EntityType entityType) {
if (CollectionUtil.isEmpty(ids)) {
return Collections.emptyList();
}
// 去重入参主键,避免重复 id 导致查询行数不等于入参数量而误判为越权
List<Long> distinctIds = ids.stream().distinct().toList();
List<T> list = service.listByIds(distinctIds);
if (AuthUtil.isAdministrator()) {
return list;
}
String currentTenantId = AuthUtil.getTenantId();
if (list.size() != distinctIds.size() || list.stream().anyMatch(entity -> !currentTenantId.equals(tenantIdOf(entity)))) {
throw new TenantException("无权操作非本租户的" + entityType.label());
}
return list;
}
/**
* 反射读取实体主键
*/
private static Long idOf(Object entity) {
Method method = ID_GETTER_CACHE.computeIfAbsent(entity.getClass(),
clazz -> requireMethod(clazz, METHOD_GET_ID));
Object value = ReflectUtil.invokeMethod(method, entity);
if (value != null && !(value instanceof Long)) {
throw new IllegalArgumentException("实体 " + entity.getClass().getSimpleName() + " 的主键类型必须为 Long");
}
return (Long) value;
}
/**
* 反射读取实体 tenantId
*/
private static String tenantIdOf(Object entity) {
Method method = TENANT_GETTER_CACHE.computeIfAbsent(entity.getClass(),
clazz -> requireMethod(clazz, METHOD_GET_TENANT_ID));
return (String) ReflectUtil.invokeMethod(method, entity);
}
/**
* 反射写入实体 tenantId
*/
private static void bindTenantId(Object entity, String tenantId) {
Method method = TENANT_SETTER_CACHE.computeIfAbsent(entity.getClass(),
clazz -> requireMethod(clazz, METHOD_SET_TENANT_ID, String.class));
ReflectUtil.invokeMethod(method, entity, tenantId);
}
private static Method requireMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
Method method = ReflectUtil.findMethod(clazz, name, paramTypes);
if (method == null) {
throw new IllegalArgumentException("实体 " + clazz.getSimpleName() + " 缺少 " + name + " 方法,不支持租户守卫");
}
return method;
}
}
@@ -0,0 +1,42 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant;
/**
* 租户id生成器
*
* @author Chill
*/
public interface TenantId {
/**
* 生成自定义租户id
*
* @return tenantId
*/
String generate();
}
@@ -0,0 +1,147 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.tenant;
import lombok.experimental.UtilityClass;
import org.springblade.core.secure.utils.AuthUtil;
import org.springframework.core.NamedThreadLocal;
import org.springframework.util.Assert;
import java.util.function.Supplier;
/**
* Tenant 工具
*
* @author L.cmBladeX
*/
@UtilityClass
public class TenantUtil {
/**
* 租户ID线程
*/
private static final ThreadLocal<String> TENANT_ID_HOLDER = new NamedThreadLocal<>("blade-tenant-id") {
@Override
protected String initialValue() {
return null;
}
};
/**
* 租户状态线程
*/
private static final ThreadLocal<Boolean> TENANT_IGNORE_HOLDER = new NamedThreadLocal<>("blade-tenant-ignore") {
@Override
protected Boolean initialValue() {
return Boolean.FALSE;
}
};
/**
* 获取租户id
*
* @return 租户id
*/
public static String getTenantId() {
String tenantId = TENANT_ID_HOLDER.get();
if (tenantId != null) {
return tenantId;
}
return AuthUtil.getTenantId();
}
/**
* 使用租户 id 执行函数
*
* @param tenantId tenantId
* @param supplier supplier
* @param <R> 泛型
* @return R 函数返回
*/
public static <R> R use(String tenantId, Supplier<R> supplier) {
Assert.hasText(tenantId, "参数 tenantId 为空");
try {
TENANT_ID_HOLDER.set(tenantId);
return supplier.get();
} finally {
TENANT_ID_HOLDER.remove();
}
}
/**
* 使用租户 id 执行函数
*
* @param tenantId tenantId
* @param runnable Runnable
*/
public static void use(String tenantId, Runnable runnable) {
Assert.hasText(tenantId, "参数 tenantId 为空");
try {
TENANT_ID_HOLDER.set(tenantId);
runnable.run();
} finally {
TENANT_ID_HOLDER.remove();
}
}
/**
* 是否忽略租户
*/
public static Boolean isIgnore() {
return TENANT_IGNORE_HOLDER.get();
}
/**
* 忽略租户 执行函数
*
* @param supplier supplier
* @param <R> 泛型
* @return R 函数返回
*/
public static <R> R ignore(Supplier<R> supplier) {
try {
TENANT_IGNORE_HOLDER.set(Boolean.TRUE);
return supplier.get();
} finally {
TENANT_IGNORE_HOLDER.remove();
}
}
/**
* 忽略租户 执行函数
*
* @param runnable Runnable
*/
public static void ignore(Runnable runnable) {
try {
TENANT_IGNORE_HOLDER.set(Boolean.TRUE);
runnable.run();
} finally {
TENANT_IGNORE_HOLDER.remove();
}
}
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.annotation;
import java.lang.annotation.*;
/**
* 排除租户数据源自动切换.
*
* @author Chill
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NonDS {
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
* 指定租户表排除.
*
* @author Chill
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Component
public @interface TableExclude {
String value() default "";
}
@@ -0,0 +1,45 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.annotation;
import java.lang.annotation.*;
/**
* 租户异步注解
*
* @author Chill
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantAsync {
/**
* 是否开启租户动态数据源
*
* @return boolean
*/
boolean datasource() default false;
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.annotation;
import java.lang.annotation.*;
/**
* 指定租户动态数据源切换.
*
* @author Chill
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantDS {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.annotation;
import java.lang.annotation.*;
/**
* 排除租户逻辑.
*
* @author Chill
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantIgnore {
}
@@ -0,0 +1,42 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.annotation;
import com.baomidou.dynamic.datasource.annotation.DS;
import java.lang.annotation.*;
/**
* 指定租户ID动态数据源切换.
*
* @author Chill
*/
@DS("#tenantId")
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantParamDS {
}
@@ -0,0 +1,131 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.aspect;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springblade.core.context.BladeContext;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tenant.TenantUtil;
import org.springblade.core.tenant.annotation.TenantAsync;
import org.springblade.core.tenant.exception.TenantException;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 租户异步切面
*
* @author Chill
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class BladeTenantAsyncAspect {
private final BladeContext bladeContext;
@Pointcut("@within(org.springblade.core.tenant.annotation.TenantAsync) || " +
"@annotation(org.springblade.core.tenant.annotation.TenantAsync)")
public void tenantAsyncPointcut() {
}
@Around("tenantAsyncPointcut()")
public Object handleTenantAsync(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取注解和租户信息
TenantAsync tenantAsync = resolveTenantAsync(joinPoint);
// 优先从认证信息获取租户ID,其次从上下文获取
String tenantId = StringUtil.isNotBlank(AuthUtil.getTenantId())
? AuthUtil.getTenantId()
: bladeContext.getTenantId();
// 无需处理租户切换的情况
if (tenantAsync == null || Func.isBlank(tenantId)) {
return joinPoint.proceed();
}
// 根据配置执行租户操作
return executeTenantOperation(joinPoint, tenantId, tenantAsync.datasource());
}
/**
* 解析TenantAsync注解
*/
private TenantAsync resolveTenantAsync(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 优先从方法获取注解
TenantAsync tenantAsync = AnnotationUtils.findAnnotation(method, TenantAsync.class);
if (tenantAsync == null) {
// 从类级别获取注解
tenantAsync = AnnotationUtils.findAnnotation(joinPoint.getTarget().getClass(), TenantAsync.class);
}
return tenantAsync;
}
/**
* 执行租户操作
*/
private Object executeTenantOperation(ProceedingJoinPoint joinPoint, String tenantId, boolean useDatasource) {
if (!useDatasource) {
// 仅切换租户上下文
return executeWithTenantContext(joinPoint, tenantId);
}
// 需要切换数据源的情况
try {
DynamicDataSourceContextHolder.push(tenantId);
return executeWithTenantContext(joinPoint, tenantId);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
/**
* 在租户上下文中执行业务逻辑
*/
private Object executeWithTenantContext(ProceedingJoinPoint joinPoint, String tenantId) {
return TenantUtil.use(tenantId, () -> {
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new TenantException(e);
}
});
}
}
@@ -0,0 +1,63 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springblade.core.tenant.BladeTenantHolder;
/**
* 自定义租户切面
*
* @author Chill
*/
@Slf4j
@Aspect
public class BladeTenantIgnoreAspect {
@Pointcut("@within(org.springblade.core.tenant.annotation.TenantIgnore) || " +
"@annotation(org.springblade.core.tenant.annotation.TenantIgnore)")
public void tenantIgnorePointcut() {
}
@Around("tenantIgnorePointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
//开启忽略
BladeTenantHolder.setIgnore(Boolean.TRUE);
//执行方法
return point.proceed();
} finally {
//关闭忽略
BladeTenantHolder.clear();
}
}
}
@@ -0,0 +1,107 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.config;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import lombok.AllArgsConstructor;
import org.springblade.core.context.BladeContext;
import org.springblade.core.mp.config.MybatisPlusConfiguration;
import org.springblade.core.tenant.*;
import org.springblade.core.tenant.aspect.BladeTenantAsyncAspect;
import org.springblade.core.tenant.aspect.BladeTenantIgnoreAspect;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
/**
* 多租户配置类
*
* @author Chill
*/
@AllArgsConstructor
@AutoConfiguration(before = MybatisPlusConfiguration.class)
@EnableConfigurationProperties(BladeTenantProperties.class)
public class TenantConfiguration {
/**
* 自定义多租户处理器
*
* @param tenantProperties 多租户配置类
* @return TenantHandler
*/
@Bean
@Primary
public TenantLineHandler bladeTenantHandler(BladeTenantProperties tenantProperties) {
return new BladeTenantHandler(tenantProperties);
}
/**
* 自定义租户拦截器
*
* @param tenantHandler 多租户处理器
* @param tenantProperties 多租户配置类
* @return BladeTenantInterceptor
*/
@Bean
@Primary
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantLineHandler tenantHandler, BladeTenantProperties tenantProperties) {
BladeTenantInterceptor tenantInterceptor = new BladeTenantInterceptor();
tenantInterceptor.setTenantLineHandler(tenantHandler);
tenantInterceptor.setTenantProperties(tenantProperties);
return tenantInterceptor;
}
/**
* 自定义租户id生成器
*
* @return TenantId
*/
@Bean
@ConditionalOnMissingBean(TenantId.class)
public TenantId tenantId() {
return new BladeTenantId();
}
/**
* 自定义租户切面
*/
@Bean
public BladeTenantIgnoreAspect bladeTenantIgnoreAspect() {
return new BladeTenantIgnoreAspect();
}
/**
* 自定义租户异步切面
*/
@Bean
public BladeTenantAsyncAspect bladeTenantAsyncAspect(BladeContext bladeContext) {
return new BladeTenantAsyncAspect(bladeContext);
}
}
@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.constant;
/**
* 租户常量.
*
* @author Chill
*/
public interface TenantBaseConstant {
/**
* 租户数据源缓存名
*/
String TENANT_DATASOURCE_CACHE = "blade:datasource";
/**
* 租户数据源缓存键
*/
String TENANT_DATASOURCE_KEY = "tenant:id:";
/**
* 租户数据源缓存键
*/
String TENANT_DATASOURCE_EXIST_KEY = "tenant:exist:";
}
@@ -0,0 +1,78 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.error;
import jakarta.servlet.Servlet;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tenant.exception.TenantDataSourceException;
import org.springblade.core.tenant.exception.TenantException;
import org.springblade.core.tool.api.R;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.DispatcherServlet;
/**
* 租户异常处理器
* <p>
* 仅当 tenant starter 上线时随 {@code TenantConfiguration} 一起激活,把租户域内的两类异常翻译到 HTTP:
* <ul>
* <li>{@link TenantException} — 越权访问 / 租户隔离违规,返回 HTTP 403 Forbidden</li>
* <li>{@link TenantDataSourceException} — 租户数据源切换失败 / 配置缺失,返回 HTTP 500 Internal Server Error</li>
* </ul>
* 优先级高于全局兜底翻译器,避免被 {@code Throwable} handler 吞掉。
*
* @author Chill
*/
@Slf4j
@AutoConfiguration
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class BladeTenantExceptionTranslator {
@ExceptionHandler(TenantException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public R handleError(TenantException e) {
log.error("租户异常", e);
return R.fail(e.getResultCode(), e.getMessage());
}
@ExceptionHandler(TenantDataSourceException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleError(TenantDataSourceException e) {
log.error("租户数据源异常", e);
return R.fail(e.getResultCode(), e.getMessage());
}
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.exception;
import lombok.Getter;
import org.springblade.core.tool.api.IResultCode;
import org.springblade.core.tool.api.ResultCode;
/**
* 租户数据源异常
*
* @author Chill
*/
public class TenantDataSourceException extends RuntimeException {
@Getter
private final IResultCode resultCode;
public TenantDataSourceException(String message) {
super(message);
this.resultCode = ResultCode.FAILURE;
}
public TenantDataSourceException(Throwable throwable) {
super(throwable);
this.resultCode = ResultCode.FAILURE;
}
public TenantDataSourceException(String message, Throwable throwable) {
super(message, throwable);
this.resultCode = ResultCode.FAILURE;
}
public TenantDataSourceException(IResultCode resultCode) {
super(resultCode.getMessage());
this.resultCode = resultCode;
}
public TenantDataSourceException(IResultCode resultCode, Throwable cause) {
super(cause);
this.resultCode = resultCode;
}
/**
* 提高性能
*
* @return Throwable
*/
@Override
public Throwable fillInStackTrace() {
return this;
}
public Throwable doFillInStackTrace() {
return super.fillInStackTrace();
}
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.exception;
import lombok.Getter;
import org.springblade.core.tool.api.IResultCode;
import org.springblade.core.tool.api.ResultCode;
/**
* 租户异常
*
* @author Chill
*/
public class TenantException extends RuntimeException {
@Getter
private final IResultCode resultCode;
public TenantException(String message) {
super(message);
this.resultCode = ResultCode.FAILURE;
}
public TenantException(Throwable throwable) {
super(throwable);
this.resultCode = ResultCode.FAILURE;
}
public TenantException(String message, Throwable throwable) {
super(message, throwable);
this.resultCode = ResultCode.FAILURE;
}
public TenantException(IResultCode resultCode) {
super(resultCode.getMessage());
this.resultCode = resultCode;
}
public TenantException(IResultCode resultCode, Throwable cause) {
super(cause);
this.resultCode = resultCode;
}
/**
* 提高性能
*
* @return Throwable
*/
@Override
public Throwable fillInStackTrace() {
return this;
}
public Throwable doFillInStackTrace() {
return super.fillInStackTrace();
}
}
@@ -0,0 +1,49 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.mp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.mp.base.BaseEntity;
/**
* 租户基础实体类
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TenantEntity extends BaseEntity {
/**
* 租户ID
*/
@Schema(description = "租户ID")
private String tenantId;
}