mybatisplus实现SaaS多租户模式共享数据库、共享数据表

 

 

 共享数据库、共享数据表,指的是多个或所有租户共享同一个数据库(Database)。所有的租户数据都存在同一个数据和同一套表中。通过数据库或表设计的租户ID租户标志字段,来表明该记录是属于哪个租户的。

优点:所有租户使用同一套数据库,所以成本低廉;能够简单进行数据聚合统计或分析。
缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量,数据备份和恢复最困难。

示例工程:

 mybatis-plus-sample-tenant

1.需要租户控制的表加tenant_id字段,不需要的在配置里面过滤掉
2.实体添加tenantId字段
3.租户表需要租户码(int),租户名称(varchar)等

package org.jeecg.config.mybatis;

import lombok.extern.slf4j.Slf4j;

/**
 * 多租户 tenant_id存储器
 */
@Slf4j
public class TenantContext {

    private static ThreadLocal currentTenant = new ThreadLocal<>();

    public static void setTenant(String tenant) {
        log.debug(" setting tenant to " + tenant);
        currentTenant.set(tenant);
    }

    public static String getTenant() {
        return currentTenant.get();
    }

    public static void clear(){
        currentTenant.remove();
    }
}
package org.jeecg.config.mybatis;

import com.baomidou.mybatisplus.core.parser.ISqlParser;
import com.baomidou.mybatisplus.core.parser.ISqlParserFilter;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import org.apache.ibatis.reflection.MetaObject;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * 单数据源配置(jeecg.datasource.open = false时生效)
 * @Author zhoujf
 *
 */
@Configuration
@MapperScan(value={"org.xxxx.modules.**.mapper*"})
public class MybatisPlusConfig {

    /**
     * tenant_id 字段名
     */
    public static final String tenant_field = "tenant_id";

    /**
     * 有哪些表需要做多租户 这些表需要添加一个字段 ,字段名和tenant_field对应的值一样
     */
    private static final List tenantTable = new ArrayList();
    /**
     * ddl 关键字 判断不走多租户的sql过滤
     */
    private static final List DDL_KEYWORD = new ArrayList();
    static {
        tenantTable.add("jee_bug_danbiao");
        DDL_KEYWORD.add("alter");
    }

    /**
     * 多租户属于 SQL 解析部分,依赖 MP 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor().setLimit(-1);
        //多租户配置 配置后每次执行sql会走一遍他的转化器 如果不需要多租户功能 可以将其注释
        tenantConfig(paginationInterceptor);
        return paginationInterceptor;
    }

    /**
     * 多租户的配置
     * @param paginationInterceptor
     */
    private void tenantConfig(PaginationInterceptor paginationInterceptor){
        /*
         * 【测试多租户】 SQL 解析处理拦截器
* 这里固定写成住户 1 实际情况你可以从cookie读取,因此数据看不到 【 麻花藤 】 这条记录( 注意观察 SQL )
*/ List sqlParserList = new ArrayList<>(); TenantSqlParser tenantSqlParser = new JeecgTenantParser(); tenantSqlParser.setTenantHandler(new TenantHandler() { @Override public Expression getTenantId(boolean select) { String tenant_id = TenantContext.getTenant(); return new LongValue(tenant_id); } @Override public String getTenantIdColumn() { return tenant_field; } @Override public boolean doTableFilter(String tableName) { //true则不加租户条件查询 false则加 // return excludeTable.contains(tableName); if(tenantTable.contains(tableName)){ return false; } return true; } private Expression in(String ids){ final InExpression inExpression = new InExpression(); inExpression.setLeftExpression(new Column(getTenantIdColumn())); final ExpressionList itemsList = new ExpressionList(); final List inValues = new ArrayList<>(2); for(String id:ids.split(",")){ inValues.add(new LongValue(id)); } itemsList.setExpressions(inValues); inExpression.setRightItemsList(itemsList); return inExpression; } }); sqlParserList.add(tenantSqlParser); paginationInterceptor.setSqlParserList(sqlParserList); paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() { @Override public boolean doFilter(MetaObject metaObject) { String sql = (String) metaObject.getValue(PluginUtils.DELEGATE_BOUNDSQL_SQL); for(String tableName: tenantTable){ String sql_lowercase = sql.toLowerCase(); if(sql_lowercase.indexOf(tableName.toLowerCase())>=0){ for(String key: DDL_KEYWORD){ if(sql_lowercase.indexOf(key)>=0){ return true; } } return false; } } /*if ("mapper路径.方法名".equals(ms.getId())) { //使用这种判断也可以避免走此过滤器 return true; }*/ return true; } }); } // /** // * mybatis-plus SQL执行效率插件【生产环境可以关闭】 // */ // @Bean // public PerformanceInterceptor performanceInterceptor() { // return new PerformanceInterceptor(); // } }
package org.jeecg.config.shiro.filters;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.config.mybatis.TenantContext;
import org.jeecg.config.shiro.JwtToken;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    private boolean allowOrigin = true;

    public JwtFilter(){}
    public JwtFilter(boolean allowOrigin){
        this.allowOrigin = allowOrigin;
    }

    /**
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            executeLogin(request, response);
            return true;
        } catch (Exception e) {
            throw new AuthenticationException("Token失效,请重新登录", e);
        }
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);

        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        if(allowOrigin){
            httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
            //update-begin-author:scott date:20200907 for:issues/I1TAAP 前后端分离,shiro过滤器配置引起的跨域问题
            // 是否允许发送Cookie,默认Cookie不包括在CORS请求之中。设为true时,表示服务器允许Cookie包含在请求中。
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
            //update-end-author:scott date:20200907 for:issues/I1TAAP 前后端分离,shiro过滤器配置引起的跨域问题
        }
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
      
        //从header获取tenant_id
        String tenant_id = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
        TenantContext.setTenant(tenant_id);
       
        return super.preHandle(request, response);
    }
}

将tenant_id写入requst headers传送到后端

config.headers[ 'tenant_id' ] = tenantid

加了租户控制的表,执行sql的时候都会带上 tenant_id = ? 字段。

注意:

多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!
启用多租户后所有执行的method的sql都会进行处理.
自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)

 

 

 

你可能感兴趣的:(物联网,mysql,java)