概述
提到多租户系统,也许大家对它的认识不一,那么到底什么是多租户系统呢。
多租户即多重租赁技术,又称为saas, 它是为共用的数据中心内如何以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍可保障客户的数据隔离。简单来说是一个单独的实例可以为多个组织服务。总之,多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离
特点
- 多个租户共享平台;
- 租户之间数据隔离;
- 租户之间发布更新互不影响;
- 签订合约租户无限扩展。
模型
1. 多租户核心概念
- 租户:一般指一个企业客户或个人客户,租户之间数据与行为是隔离的。
- 用户:在某个租户内的具体使用者,可以通过使用账户名、密码等登录信息,登录到SaaS系统使用软件服务。
- 组织:如果租户是一个企业客户,通常会拥有自己的组织架构。
- 员工:是指组织内部具体的某位员工。
- 解决方案:为了解决客户的某类型业务问题,SaaS服务商将产品与服务组合在一起,为商家提供整体的打包方案。
- 产品能力:指的是SaaS服务商对客户售卖的产品应用,特指能够帮助客户实现端到端场景解决方案闭环的能力。
- 资源域:用来运行1个或多个产品应用的一套云资源环境。
- 云资源:SaaS产品一般都部署在各种云平台上,例如阿里云、腾讯云、华为云等。对这些云平台提供的计算、存储、网络、容器等资源,抽象为云资源。
2. 概念模型设计
- SaaS平台可以创建与管理多个平台用户、多个租户、多个资源域。
- 单个平台用户可以关联到多个租户下,例如,平台用户张三,可以是租户A的用户,也可以是租户B的用户。单个租户下可以拥有多个用户。
- 单个租户可以订购多个解决方案,解决方案可以包多个产品能力,产品能力运行在某个资源域上。
- 组织单元间有上下级关系,单个组织下可以有多个员工,员工与单个用户进行绑定。
区别
传统软件项目一般是指,面向客户开发一套特定的软件系统,并部署在独立的环境中,通常是企业内部环境。而SaaS模式,是将软件服务部署到云端环境,可以面向不同的客户提供相同的软件服务
1. 开发和交互模式:传统软件的开发模式是以软件产品为中心,通过市场推广不断寻找更多的客户购买产品来实现业务增长,一般通过磁盘等固体介质或者以软件下载安装传播,并且软件需要安装到使用者的电脑上。而在SaaS模式中,厂家会先把软件安装在自己的服务器上,客户不需要安装任何类似传统模式的客户端软件,而且基于云端,只要有设备能够连接并浏览互联网,客户就可以“随时随地”通过手机、电脑、IPAD等多种方式接入软件系统,从而进行操作和管理。
2. 部署方式:传统软件采用本地部署方式,部署时间较长,会耗费较多的IT资源和时间。而SaaS软件采用云端部署方式,部署时间相对较短,可快速上线使用,比如针对软件中的报销审批这一特定功能的部署只需要一周多的时间,大型的SaaS软件部署最多也不会超过90天。
3. 数据安全性:传统软件都是安装在用户自己的服务器上,掌握在自己手里,数据基本处于可控状态,稳定性和安全性都会有很大的保障。而SaaS软件的数据是存放在SaaS平台供应商的服务器上,数据不受自己控制,也不能对运维的细节进行控制,比如基础架构的备份、灾难恢复和安全策略等,数据的隐私性和安全性存在一定的隐患。
4. 商业模式:传统软件的付费模式,通常都会包含:一次性投入数额高昂的购买费、安装费、维护费、管理费等,所以传统软件生产商最主要的盈利模式便是通过销售软件产品来盈利。而SaaS不需要一次性购买软件,而是将软件采购变成了软件租赁,信息化门槛降低,迅速覆盖了中小微企业市场,如果软件无法满足需求,客户可以随时和SaaS供应商终止合作。SaaS以标准产品为主,标准化才能规模化。
5. 集成性:传统软件的集成性就相对较强,通常一个软件可以关联很多系统。而SaaS用户对应用程序细节的控制非常少,甚至没有,比如对版本、支持的特性和额外插件的控制。这会对SaaS产品与现有的本地应用程序和数据源或者其他SaaS产品的集成带来一些困难。
架构参考
原理
1. 租户内部模型关系
对SaaS产品来说,租户是最顶层的概念,租户内部拥有组织、用户、产品能力、云资源等模型,租户就像租了一套大房子,其他模型都是房子内部的家具或设施。
2. 租户身份识别
在各种隔离模式下,识别租户身份,获取租户的资源配置,是非常关键的。当一个用户登录SaaS系统后,系统会返回租户上下文信息,上下文会包含用户绑定的租户信息,以及隔离模式。租户上下文信息会被附加在每一次系统交互中,贯穿整个系统调用链路,让上游调用方知道路由到哪些下游资源。
3. 租户计费计量管理
在竖井隔离模式下,由于资源本身就是隔离的,所以可以根据占用的计算、存储、网络资源来计费计量,逻辑相对简单。在共享模式下,计费计量就比较复杂,我们要能准确地采集到各个租户对实际资源的使用情况,一般会根据请求并发量、存储容量、数据对象数量等数据来进行组合计费。
实现思路
在SaaS模式中,多租户之间最基础的能力则是资源隔离,出于成本和运营效率考虑,SaaS服务商需要构建多租户能同时访问软件的环境,不同租户能访问同一套SaaS产品服务,但对资源访问要进行严格的隔离。
SaaS资源隔离包含几个层次:
第一层是隔离SaaS系统底层所涉及到的计算、存储、网络等资源;
第二层是隔离系统的基础数据,如:组织、用户、角色、权限、产品能力授权关系等;
第三层是系统使用过程中,各类业务动态数据的隔离,如:业务单据、操作记录等。
多租户架构主要是解决第一层的隔离问题。为了实现多租户隔离架构,下面我们来看看常见的几种多租户隔离模式。
1. 竖井隔离模式
有些SaaS服务商会选择竖井隔离模式,即每个租户都运行在隔离的一组资源中。有人会说,这不就是传统软件模式吗,为什么会是SaaS模式呢?因为这些竖井式的资源,如果是拥有标准化的租户身份识别、入驻流程、计费体系、部署流程、运营流程,那它依然是SaaS模式,只不过每个客户都有一套端到端的基础设施。
优势:
(1)满足强隔离需求:一些客户为了系统和数据的安全性,可能提出非常严格的隔离需求,期望软件产品能够部署在一套完全独立的环境中,不和其他租户的应用实例、数据放在一起;
(2)计费逻辑简单:SaaS服务商需要针对租户使用资源进行计费,对于复杂的业务场景,计算、存储、网络资源间的关系同样也会非常复杂,计费模型是很有挑战的,但在竖井模式下,计费模型相对来说是比较简单的;
(3)降低故障影响:因为每个客户的系统都部署在自己的环境中,如果其中一个环境出现故障,并不会影响其他客户使用软件服务。
劣势:
(1)规模化问题:由于租户的SaaS环境是独立的,所以每入驻一个租户,就需要创建和运营一套SaaS环境,如果只是少量的租户,还可能可以管理,但如果是成千上万的租户,管理和运营这些环境将会是非常大的挑战;
(2)成本问题:每个租户都有独立的环境,花费在单个客户上的成本将非常高,会大幅度削弱SaaS软件服务的盈利能力;
(3)敏捷迭代问题:SaaS模式的一个优势是能够快速响应市场需求,迭代产品功能。但竖井隔离策略会阻碍这种敏捷迭代能力,因为更新、管理、支撑这些租户的SaaS环境,会变得非常复杂和低效;
(4)系统管理与监控:在同一套环境中,对部署的基础设施进行管理与监控,是较为简单的。但每个租户都有独立的环境,在这种非中心化的模式下,对每个租户的基础设施进行管理与监控,同样也是非常复杂、困难的。
2. 共享模式
据信,许多SaaS服务商会优先选择共享模式,即多租户共享一套基础设施资源,这将使SaaS软件服务更加高效、灵活、低成本。
优势:
(1)高效管理:在共享策略下,所有租户都可以集中管理并以高管理效率运营。同时,管理和监控基础结构配置将更容易。相比竖井策略,产品的迭代更新会更快;
(2)低成本:SaaS服务商的成本结构很大一块是基础设施成本。在共享模型下,服务商可以根据租户们的实际资源负载动态扩展系统,这样基础设施的利用率将非常高。
劣势:
(1)租户相互影响:因为所有租户共享一组资源,当一个租户占用大量机器资源时,其他租户的使用体验很可能受到影响,在这种情况下,就有必要对技术架构,设计一些限制(限流、降级、服务器隔离等),以使影响可控;
(2)租户计费困难:在竖井模型下,非常容易计算租户的资源消耗。然而,在共享模型下,因为所有租户共享一组资源,所以需要更多的精力来计算单个租户的合理成本。
3. 分域隔离模式
传统的大型企业更喜欢私有化部署和个性化交付的传统模式,因为他们需要更强的管控和更高的安全性。然而,中小企业支付能力有限,他们的需求往往更加标准化,因此他们更喜欢便宜、更简单的SaaS产品。为了满足不同客户的需求,还有一种将竖井模型与共享模型相结合的模式,即分域隔离模式。
在此模式中,基础域和专用域被细分。基础域是使用共享模型,所有租户共享一组套资源,专用域是使用竖井模型,每个租户都有独立的资源环境。对于大多数中小型客户,他们都在基础域环境中使用SaaS产品。只有少数大客户会在特殊领域使用SaaS产品。一般来说,他们这种是有很强的支付能力和很强的隔离需求。
但是,需要注意的是,为了避免多个产品版本,SaaS服务商需要确保基础域、专用域的产品版本一致,个性化部分应尽可能通过PaaS平台构建,以便于ISV参与建设。否则,一旦SaaS产品的标准化降低,后续版本的维护将会成为一场灾难。
方案
方案如下:
- 独立数据库
- 共享数据库、独立Schema
- 共享数据库、共享Schema、共享数据表
1.独立数据库
这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。
优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
缺点: 增多了数据库的安装数量,随之带来维护成本和购置成本的增加。
这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户,可以选择这种模式,提高租用的定价。如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。
2.共享数据库,独立 Schema
这是第二种方案,即多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。底层库比如是:DB2、ORACLE等,一个数据库下可以有多个SCHEMA
优点: 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
缺点: 如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据; 如果需要跨租户统计数据,存在一定困难。
3 共享数据库,共享 Schema,共享数据表
这是第三种方案,即租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。
即每插入一条数据时都需要有一个客户的标识。这样才能在同一张表中区分出不同客户的数据。
优点:三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。
缺点: 隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量; 数据备份和恢复最困难,需要逐表逐条备份和还原。如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种方案最适合。
4 总结
在SaaS实施过程中,有一个显著的考量点,就是如何对应用数据进行设计,以支持多租户,而这种设计的思路,是要在数据的共享、安全隔离和性能间取得平衡。三种模式的特点可以用一张图来概括
5 代码实现共享数据库,共享 Schema,共享数据表
分层级-级别,上级权限包含所有下级的权限,示例如下:
集团;可以包含多个公司。
公司;可以包含多个员工。
员工(租户);
3.1 pom.xml配置
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ybw</groupId>
<artifactId>share-table</artifactId>
<version>1.0.0</version>
<name>share-table</name>
<description>多租户-共享table</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.4</spring-boot.version>
<mybatis-plus.version>3.4.1</mybatis-plus.version>
<velocity.version>2.3</velocity.version>
<druid.version>1.2.12</druid.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.14</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity-engine-core -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.ybw.mybatis.multi.tenant.MybatisMultiTenantApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3.2 数据库初始化
/*
Navicat Premium Data Transfer
Source Server : 本地
Source Server Type : MySQL
Source Server Version : 80030
Source Host : localhost:3306
Source Schema : tenant
Target Server Type : MySQL
Target Server Version : 80030
File Encoding : 65001
Date: 09/10/2022 16:48:33
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_staff
-- ----------------------------
DROP TABLE IF EXISTS `sys_staff`;
CREATE TABLE `sys_staff` (
`id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT,
`level` int(0) NOT NULL COMMENT '级别 1:集团;2:公司;3:员工(租户);',
`pid` bigint(0) NULL DEFAULT NULL COMMENT '父id,sys_staff的id',
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_pid`(`pid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '员工' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_staff
-- ----------------------------
INSERT INTO `sys_staff` VALUES (1, 1, NULL, '2022-10-09 14:23:52', '2022-10-09 14:23:52');
INSERT INTO `sys_staff` VALUES (2, 2, 1, '2022-10-09 14:23:57', '2022-10-09 14:23:57');
INSERT INTO `sys_staff` VALUES (3, 3, 2, '2022-10-09 14:24:16', '2022-10-09 14:24:16');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名',
`company_group_id` bigint(0) NOT NULL COMMENT '租户-集团ID',
`company_id` bigint(0) NOT NULL COMMENT '租户-公司id',
`tenant_id` bigint(0) NOT NULL COMMENT '租户ID',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_tenant_id`(`tenant_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'Jone', 1, 1, 1);
INSERT INTO `user` VALUES (2, 'Jack', 1, 1, 2);
INSERT INTO `user` VALUES (3, 'Tom', 1, 2, 3);
INSERT INTO `user` VALUES (4, 'Sandy', 2, 3, 4);
INSERT INTO `user` VALUES (5, 'Billie', 2, 4, 5);
-- ----------------------------
-- Table structure for user_addr
-- ----------------------------
DROP TABLE IF EXISTS `user_addr`;
CREATE TABLE `user_addr` (
`id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(0) NOT NULL COMMENT 'user.id',
`addr` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '地址名称',
`company_group_id` bigint(0) NOT NULL COMMENT '租户-集团ID',
`company_id` bigint(0) NOT NULL COMMENT '租户-公司id',
`tenant_id` bigint(0) NOT NULL COMMENT '租户ID',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_addr
-- ----------------------------
INSERT INTO `user_addr` VALUES (1, 1, 'addr1', 1, 1, 1);
INSERT INTO `user_addr` VALUES (2, 1, 'addr2', 1, 2, 3);
SET FOREIGN_KEY_CHECKS = 1;
3.3 接口
package com.ybw.controller;
import com.ybw.entity.SysStaff;
import com.ybw.service.SysStaffService;
import com.ybw.service.token.TokenService;
import com.ybw.utils.MyStringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 员工
*
* @author ybw
* @since 2022-10-07
*/
@RestController
@RequestMapping("/sysStaff")
public class SysStaffController {
@Resource
private SysStaffService sysStaffService;
@Resource
private TokenService tokenService;
/**
* 获取token
*
* @param id
* @methodName: getToken
* @return: java.lang.String
* @author: ybw
* @date: 2022/10/9
**/
@GetMapping("/getToken")
public String getToken(Long id) {
SysStaff sysStaff = sysStaffService.getById(id);
if (sysStaff == null) {
return null;
}
String token = MyStringUtils.generateUUIDNoCenterLine();
tokenService.generateToken(token, sysStaff);
return token;
}
}
package com.ybw.controller;
import com.ybw.entity.User;
import com.ybw.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* 用户
*
* @author ybw
* @since 2022-01-10
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@GetMapping("/getUser")
public List<User> getUser() {
return userService.lambdaQuery().list();
}
/**
* 测试多租户
* 1、left join: SELECT u.id, u.name, a.addr AS addr_name FROM user u LEFT JOIN user_addr a ON a.user_id = u.id AND a.company_group_id = 1 WHERE u.name LIKE concat(concat('%', ?), '%') AND u.company_group_id = 1
* 2、inner join: SELECT u.id, u.name, a.addr AS addr_name FROM user u, user_addr a WHERE a.user_id = u.id AND u.name LIKE concat(concat('%', ?), '%') AND u.company_group_id = 1 AND a.company_group_id = 1
*
* @param username
* @methodName: getUserAndAddr
* @return: java.util.List<com.ybw.entity.User>
* @author: ybw
* @date: 2023/6/13
**/
@GetMapping("/getUserAndAddr")
public List<User> getUserAndAddr(@RequestParam String username) {
return userService.getUserAndAddr(username);
}
}
3.4 鉴权
package com.ybw.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ybw.config.TenantContext;
import com.ybw.dto.TenantDTO;
import com.ybw.entity.SysStaff;
import com.ybw.service.token.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
/**
* 接口日志
*
* @author ybwei
* @date 2022/2/17 11:40
**/
@Aspect
@Component
@Slf4j
public class AuthAspect {
@Resource
private TokenService tokenService;
private List<String> ignoreUrlList = Arrays.asList("/sysStaff/getToken");
private final ObjectMapper mapper;
@Autowired
public AuthAspect(ObjectMapper mapper) {
this.mapper = mapper;
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void requestLog() {
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postLog() {
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void getLog() {
}
/**
* 请求参数
*
* @param joinPoint
* @return void
* @throws
* @methodName: doBefore
* @author ybwei
* @date 2022/2/17 13:54
*/
@Before("requestLog() || postLog() || getLog()")
public void doBefore(JoinPoint joinPoint) {
//1、获取访问路径
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String servletPath = request.getServletPath();
if (ignoreUrlList.contains(servletPath)) {
//1.1 不需要鉴权
return;
}
//2、获取token
String token = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("token");
if (StringUtils.isBlank(token)) {
return;
}
//3、获取员工信息
SysStaff sysStaff = tokenService.getObjectByToken(token);
if (sysStaff == null) {
return;
}
//4、处理多租户
TenantContext.set(new TenantDTO(sysStaff.getLevel(), sysStaff.getId()));
}
/**
* 返回参数
*
* @param response
* @return void
* @throws
* @methodName: doAfterReturning
* @author ybwei
* @date 2022/2/17 13:54
*/
@AfterReturning(returning = "response", pointcut = "requestLog() || postLog() || getLog()")
public void doAfterReturning(Object response) {
TenantContext.remove();
}
}
3.5 多租户配置
使用的是mybatis plus多租户插件
package com.ybw.config;
import com.ybw.dto.TenantDTO;
/**
* @author ybw
* @version V1.0
* @className TenantContext
* @date 2022/9/30
**/
public class TenantContext {
private static final ThreadLocal<TenantDTO> context = new ThreadLocal<>();
/**
* 构造方法私有化
*
* @methodName: TenantContext
* @return:
* @author: ybw
* @date: 2022/9/30
**/
private TenantContext() {
}
/**
* 存放租户信息
*
* @param tenantDTO
* @methodName: set
* @return: void
* @author: ybw
* @date: 2022/9/30
**/
public static void set(TenantDTO tenantDTO) {
context.set(tenantDTO);
}
/**
* 获取组合信息
*
* @methodName: get
* @return: com.ybw.dto.TenantDTO
* @author: ybw
* @date: 2022/9/30
**/
public static TenantDTO get() {
return context.get();
}
/**
* 清除当前线程内引用,防止内存泄漏
*
* @methodName: remove
* @return: void
* @author: ybw
* @date: 2022/9/30
**/
public static void remove() {
context.remove();
}
}
package com.ybw.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.ybw.contant.LevelConstant;
import com.ybw.contant.TenantConstant;
import com.ybw.dto.TenantDTO;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* 多租户配置
*
* @author ybw
* @version V1.0
* @className MybatisPlusConfig
* @date 2022/10/10
**/
@Configuration
@MapperScan("com.ybw.mapper")
public class MybatisPlusConfig {
@Resource
private TenantIgnoreConfig tenantIgnoreConfig;
/**
* 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//1、租户插件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
*
* @return 租户 ID 值表达式
*/
@Override
public Expression getTenantId() {
TenantDTO tenantDTO = TenantContext.get();
if (tenantDTO == null) {
//默认为0
return new LongValue(0);
}
return new LongValue(tenantDTO.getTenantId());
}
/**
* 获取租户字段名
* <p>
* 默认字段名叫: tenant_id
*
* @return 租户字段名
*/
@Override
public String getTenantIdColumn() {
TenantDTO tenantDTO = TenantContext.get();
if (LevelConstant.COMPANY_GROUP.equals(tenantDTO.getLevel())) {
return TenantConstant.Field.COMPANY_GROUP_ID;
}
if (LevelConstant.COMPANY.equals(tenantDTO.getLevel())) {
return TenantConstant.Field.COMPANY_ID;
}
return TenantConstant.Field.TENANT_ID;
}
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
*
* 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
if (tenantIgnoreConfig.getTableList().contains(tableName)) {
return true;
}
return false;
}
}));
//2、分页插件
// 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}