若依框架自带了 Spring Security 权限管理,但没有原生支持多租户,目前的需求就是改为多租户的形式
若依框架修改为多租户
- 前端部分
- 1. 登录页面
- 1.1 新增租户选择项
- 3. 菜单和权限处理
- 3.1 根据租户动态加载菜单
- 4. 数据展示和操作
- 4.1 处理不同租户的数据
- 5. 全局状态管理
- 5.1 存储租户信息
- 后端部分
- 1. 多租户实现思路
- 2. 数据库设计修改
- 2.1 新增租户表
- 2.2 修改用户表
- 3. 请求层面处理
- 3.1 拦截请求获取租户标识
- 3.2 租户上下文工具类
- 4. 权限层面处理
- 4.1 修改 Spring Security 配置
- 4.2 修改权限验证逻辑
- 5. 数据访问层面处理
- 6. 配置拦截器或过滤器
前端部分
1. 登录页面
1.1 新增租户选择项
在登录页面添加一个租户选择框,让用户在登录时选择所属的租户。可以从后端获取租户列表,填充到选择框中。
<template>
<div>
<!-- 用户名输入框 -->
<el-input v-model="username" placeholder="请输入用户名"></el-input>
<!-- 密码输入框 -->
<el-input v-model="password" type="password" placeholder="请输入密码"></el-input>
<!-- 租户选择框 -->
<el-select v-model="tenantId" placeholder="请选择租户">
<el-option
v-for="tenant in tenantList"
:key="tenant.tenantId"
:label="tenant.tenantName"
:value="tenant.tenantId">
</el-option>
</el-select>
<!-- 登录按钮 -->
<el-button @click="login">登录</el-button>
</div>
</template>
<script>
export default {
data() {
return {
username: '',
password: '',
tenantId: null,
tenantList: []
};
},
created() {
// 获取租户列表
this.getTenantList();
},
methods: {
getTenantList() {
// 调用后端接口获取租户列表
this.$axios.get('/api/tenant/list').then(response => {
this.tenantList = response.data;
});
},
login() {
// 登录请求,携带租户 ID
this.$axios.post('/api/login', {
username: this.username,
password: this.password,
tenantId: this.tenantId
}).then(response => {
// 处理登录成功逻辑
});
}
}
};
</script>
- 请求拦截器
2.1 添加租户信息到请求头
在前端的请求拦截器中,将当前租户 ID 添加到请求头中,以便后端能够识别请求所属的租户。
import axios from 'axios';
// 创建 axios 实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 获取当前租户 ID
const tenantId = localStorage.getItem('tenantId');
if (tenantId) {
// 将租户 ID 添加到请求头
config.headers['X-Tenant-ID'] = tenantId;
}
return config;
},
error => {
console.log(error);
Promise.reject(error);
}
);
export default service;
3. 菜单和权限处理
3.1 根据租户动态加载菜单
不同租户可能有不同的菜单和权限,前端需要根据当前租户动态加载菜单。可以在登录成功后,根据租户 ID 请求后端获取该租户的菜单信息。
// 登录成功后,获取当前租户的菜单信息
this.$axios.get('/api/menu/list', {
params: {
tenantId: this.tenantId
}
}).then(response => {
// 动态生成菜单
this.menuList = response.data;
});
4. 数据展示和操作
4.1 处理不同租户的数据
在展示和操作数据时,前端需要确保只显示和操作当前租户的数据。可以在请求数据时,将租户 ID 作为参数传递给后端,后端根据租户 ID 进行数据过滤。
// 获取当前租户的用户列表
this.$axios.get('/api/user/list', {
params: {
tenantId: this.tenantId
}
}).then(response => {
this.userList = response.data;
});
5. 全局状态管理
5.1 存储租户信息
使用 Vuex 或其他状态管理工具,存储当前租户的信息,方便在整个应用中使用。
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
tenantId: null,
tenantName: ''
},
mutations: {
setTenantInfo(state, tenant) {
state.tenantId = tenant.tenantId;
state.tenantName = tenant.tenantName;
// 存储到本地存储
localStorage.setItem('tenantId', tenant.tenantId);
}
},
actions: {
login({ commit }, tenant) {
commit('setTenantInfo', tenant);
}
}
});
后端部分
1. 多租户实现思路
多租户(Multi - Tenancy)是一种软件架构技术,它允许多个租户(客户)共享同一套软件系统,同时又能保证各个租户的数据相互隔离。在若依框架中实现多租户,可以从以下几个方面入手:
数据库层面:采用租户标识区分不同租户的数据。
请求层面:在请求中携带租户标识,以便在处理请求时区分不同租户。
权限层面:不同租户的用户具有不同的权限。
2. 数据库设计修改
2.1 新增租户表
在数据库中新增一个租户表,用于存储租户的基本信息。
CREATE TABLE `sys_tenant` (
`tenant_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '租户 ID',
`tenant_name` varchar(100) NOT NULL COMMENT '租户名称',
`tenant_code` varchar(50) NOT NULL COMMENT '租户编码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户表';
2.2 修改用户表
在用户表中新增一个租户 ID 字段,用于关联租户。
ALTER TABLE `sys_user` ADD COLUMN `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户 ID';
3. 请求层面处理
3.1 拦截请求获取租户标识
可以通过自定义拦截器或过滤器来获取请求中的租户标识,并将其存储在上下文中。
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String tenantId = httpRequest.getHeader("X-Tenant-ID");
if (tenantId != null) {
// 将租户 ID 存储在上下文中
TenantContextHolder.setTenantId(Long.parseLong(tenantId));
}
try {
chain.doFilter(request, response);
} finally {
// 清除上下文
TenantContextHolder.clearTenantId();
}
}
}
3.2 租户上下文工具类
创建一个租户上下文工具类,用于存储和获取当前请求的租户 ID。
public class TenantContextHolder {
private static final ThreadLocal<Long> tenantIdThreadLocal = new ThreadLocal<>();
public static void setTenantId(Long tenantId) {
tenantIdThreadLocal.set(tenantId);
}
public static Long getTenantId() {
return tenantIdThreadLocal.get();
}
public static void clearTenantId() {
tenantIdThreadLocal.remove();
}
}
4. 权限层面处理
4.1 修改 Spring Security 配置
在 Spring Security 配置中,添加租户验证逻辑。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic()
.and()
.addFilterBefore(new TenantFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
4.2 修改权限验证逻辑
在权限验证时,需要考虑租户的影响。例如,在查询用户权限时,需要根据租户 ID 进行过滤。
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Long tenantId = TenantContextHolder.getTenantId();
// 根据租户 ID 和用户名查询用户信息和权限
// ...
return null;
}
}
5. 数据访问层面处理
在数据访问层,需要在查询语句中添加租户 ID 条件,以确保不同租户的数据相互隔离。
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserMapper {
@Select("SELECT * FROM sys_user WHERE username = #{username} AND tenant_id = #{tenantId}")
List<User> findUsersByUsernameAndTenantId(String username, Long tenantId);
}
6. 配置拦截器或过滤器
在 Spring Boot 配置类中注册租户过滤器。
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration() {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new TenantFilter());
registration.addUrlPatterns("/*");
registration.setName("tenantFilter");
registration.setOrder(1);
return registration;
}
}