SpringCloud microservice practice -- building an enterprise development framework: realizing multi tenant function based on MybatisPlus plug-in TenantLineInnerInterceptor

Basic concepts of multi tenant Technology:
Multi tenancy Technology (English: multi tenancy Technology), or multi tenancy technology, is a software architecture technology. It discusses and implements how to share the same system or program components in a multi-user environment, and can still ensure the isolation of data among users.
  with the blessing of cloud computing, multi tenant technology is widely used to develop various cloud services. You can see the shadow of multi tenant technology whether IaaS, PaaS or SaaS.
    as described earlier, the GitEgg framework interacts with the database and uses the mybatis plus enhancement tool. Mybatis plus provides TenantLineInnerInterceptor tenant processor to realize multi tenant function. Its principle is that mybatis plus implements a custom mybatis Interceptor, and automatically adds tenant query conditions, actual and paging plug-ins after the sql to be executed, The data rights Interceptor is implemented in the same way.

In short, multi tenant technology allows a system to provide services to different customers through configuration. The data seen by each customer belongs to itself, just as each customer has its own independent and perfect system.

The following is the application configuration in GitEgg system:

1. Create new multi tenant component configuration files TenantProperties.java and TenantConfig.java under gitegg platform mybatis project. TenantProperties.java is used by the system to read the configuration file. Here, the specific configuration information of multiple groups of users will be set in the Nacos configuration center. TenantConfig.java is the configuration that the plug-in needs to read. There are three configuration items:
TenantId, tenant ID, TenantIdColumn, multi tenant field name, ignoreTable, tables that do not require multi tenant isolation.
TenantProperties.java:

package com.gitegg.platform.mybatis.props;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * White list configuration
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {

    /**
     * Enable tenant mode
     */
    private Boolean enable;

    /**
     * Multi tenant field name
     */
    private String column;

    /**
     * Multi tenant tables that need to be excluded
     */
    private List<string> exclusionTable;

}

TenantConfig.java:

package com.gitegg.platform.mybatis.config;

import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.gitegg.platform.boot.util.GitEggAuthUtils;
import com.gitegg.platform.mybatis.props.TenantProperties;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.NullValue;
import net.sf.jsqlparser.expression.StringValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * Multi tenant configuration center
 *
 * @author GitEgg
 */
@Configuration
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@AutoConfigureBefore(MybatisPlusConfig.class)
public class TenantConfig {

	private final TenantProperties tenantProperties;

	/**
	 * The new multi tenant plug-in configuration follows the rules of mybatis,
	 * Mybatisconfiguration#usedeprecetedexecutor = false needs to be set
	 * Avoid caching in case of problems
	 *
	 * @return TenantLineInnerInterceptor
	 */
	@Bean
	public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
		return new TenantLineInnerInterceptor(new TenantLineHandler() {
			/**
			 * Get tenant ID
			 * @return Expression
			 */
			@Override
			public Expression getTenantId() {
				String tenant = GitEggAuthUtils.getTenantId();
				if (tenant != null) {
					return new StringValue(GitEggAuthUtils.getTenantId());
				}
				return new NullValue();
			}

			/**
			 * Gets the field name of the multi tenant
			 * @return String
			 */
			@Override
			public String getTenantIdColumn() {
				return tenantProperties.getColumn();
			}

			/**
			 * Filter tables that do not need to be isolated according to tenants
			 * This is the default method, which returns false by default, indicating that all tables need to spell multi tenant conditions
			 * @param tableName Table name
			 */
			@Override
			public boolean ignoreTable(String tableName) {
				return tenantProperties.getExclusionTable().stream().anyMatch(
						(t) -> t.equalsIgnoreCase(tableName)
				);
			}
		});
	}
}

2. You can create a new application.yml under the project to configure the information to be configured on Nacos in the future:

tenant:
  # Enable tenant mode
  enable: true
  # Multi tenant tables that need to be excluded
  exclusionTable:
    - "t_sys_district"
    - "oauth_client_details"
  # Tenant field name
  column: tenant_id

3. Modify MybatisPlusConfig.java and load the multi tenant filter to make it effective:

package com.gitegg.platform.mybatis.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.gitegg.platform.mybatis.props.TenantProperties;
import lombok.RequiredArgsConstructor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@MapperScan("com.gitegg.**.mapper.**")
public class MybatisPlusConfig {

    private final TenantLineInnerInterceptor tenantLineInnerInterceptor;

    private final TenantProperties tenantProperties;

    /**
     * The new paging plug-in follows the rules of mybatis. MybatisConfiguration#useDeprecatedExecutor = false is required
     * Avoid cache problems (this attribute will be removed after the old plug-in is removed)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {

        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        //Multi tenant plug-in
        if (tenantProperties.getEnable()) {
            interceptor.addInnerInterceptor(tenantLineInnerInterceptor);
        }

        //Paging plug-in
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        //Plug in for preventing full table update and deletion: BlockAttackInnerInterceptor
        BlockAttackInnerInterceptor blockAttackInnerInterceptor = new BlockAttackInnerInterceptor();
        interceptor.addInnerInterceptor(blockAttackInnerInterceptor);

        return interceptor;
    }

    /**
     * When the optimistic lock plug-in wants to update a record, it hopes that this record has not been updated by others
     * https://mybatis.plus/guide/interceptor-optimistic-locker.html#optimisticlockerinnerinterceptor
     */
    @Bean
    public OptimisticLockerInnerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInnerInterceptor();
    }

}

4. A public method for obtaining tenant information is added in GitEggAuthUtils method. Tenant information is set when forwarding through Gateway. How to set tenant information into Header will be described later:

package com.gitegg.platform.boot.util;

import cn.hutool.json.JSONUtil;
import com.gitegg.platform.base.constant.AuthConstant;
import com.gitegg.platform.base.domain.GitEggUser;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

public class GitEggAuthUtils {

    /**
     * Get user information
     *
     * @return GitEggUser
     */
    public static GitEggUser getCurrentUser() {
        HttpServletRequest request = GitEggWebUtils.getRequest();
        if (request == null) {
            return null;
        }
        try {
            String user = request.getHeader(AuthConstant.HEADER_USER);
            if (StringUtils.isEmpty(user))
            {
                return null;
            }
            String userStr = URLDecoder.decode(user,"UTF-8");
            GitEggUser gitEggUser = JSONUtil.toBean(userStr, GitEggUser.class);
            return gitEggUser;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }

    }

    /**
     * Get tenant Id
     *
     * @return tenantId
     */
    public static String getTenantId() {
        HttpServletRequest request = GitEggWebUtils.getRequest();
        if (request == null) {
            return null;
        }
        try {
            String tenantId = request.getHeader(AuthConstant.TENANT_ID);
            String user = request.getHeader(AuthConstant.HEADER_USER);
            //If the tenantId in the request header is empty, try to get the tenant id from the logged in user
            if (StringUtils.isEmpty(tenantId) && !StringUtils.isEmpty(user))
            {
                String userStr = URLDecoder.decode(user,"UTF-8");
                GitEggUser gitEggUser = JSONUtil.toBean(userStr, GitEggUser.class);
                if (null != gitEggUser)
                {
                    tenantId = gitEggUser.getTenantId();
                }
            }
            return tenantId;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }

    }
}

5. AuthGlobalFilter of gitegg gateway sub project in gitegg cloud project adds the filtering method of setting TenantId

        String tenantId = exchange.getRequest().getHeaders().getFirst(AuthConstant.TENANT_ID);

        String token = exchange.getRequest().getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);

        if (StrUtil.isEmpty(tenantId) && StrUtil.isEmpty(token)) {
            return chain.filter(exchange);
        }

        Map<string, string=""> addHeaders = new HashMap<>();

        // If the system configuration has enabled tenant mode, set tenantId
        if (enable && StrUtil.isEmpty(tenantId)) {
            addHeaders.put(AuthConstant.TENANT_ID, tenantId);
        }

6. The above steps are the multi tenant function integration steps in the background. In the actual project development process, we need to consider the configuration and implementation ideas of the front-end page on the tenant information. The unused tenants have different domain names. The front-end page obtains the corresponding tenant information according to the current domain name and sets the TenantId parameter in the public request method, Ensure that each request can carry tenant information.

// request interceptor
request.interceptors.request.use(config => {
  const token = storage.get(ACCESS_TOKEN)
  // If the token exists
  // Let each request carry a custom token. Please modify it according to the actual situation
  if (token) {
    config.headers['Authorization'] = token
  }
  config.headers['TenantId'] = process.env.VUE_APP_TENANT_ID
  return config
}, errorHandler)
Source address:  

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg

Tags: Spring Cloud Microservices

Posted on Fri, 26 Nov 2021 06:43:26 -0500 by kazer