当前位置:   article > 正文

基于Mybatis-Plus的多租户&数据权限隔离(全网最优)

数据权限隔离

标题党了;哈哈!!!

1.多租户插件

我们来看mybatis-Plus 提供的多租户插件还是很方便的: 多租户插件 | MyBatis-Plus

官方的例子其实已经很清晰,来看下我们实战中的例子;

首先是属性配置类:

  1. import java.util.List;
  2. import lombok.Data;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import org.springframework.context.annotation.Configuration;
  5. /**
  6. * 白名单配置
  7. */
  8. @Data
  9. @Configuration
  10. @ConfigurationProperties(prefix = "tenant")
  11. public class TenantProperties {
  12. /**
  13. * 是否开启租户模式
  14. */
  15. private Boolean enable;
  16. /**
  17. * 多租户字段名称
  18. */
  19. private String column;
  20. /**
  21. * 需要排除的多租户的表
  22. */
  23. private List<String> exclusionTable;
  24. }

接下来是bean配置类

  1. package com.caicongyang.hc.conf;
  2. import com.baomidou.mybatisplus.annotation.DbType;
  3. import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
  4. import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
  5. import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
  6. import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
  7. import net.sf.jsqlparser.expression.Expression;
  8. import net.sf.jsqlparser.expression.StringValue;
  9. import org.apache.ibatis.plugin.Interceptor;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.context.annotation.Bean;
  12. import org.springframework.context.annotation.Configuration;
  13. import java.util.Objects;
  14. @Configuration
  15. public class MybatisPlusConfiguration {
  16. @Autowired
  17. TenantProperties tenantProperties;
  18. @Bean
  19. public Interceptor paginationInnerInterceptor() {
  20. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  21. if (tenantProperties.getEnable()) {
  22. interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
  23. @Override
  24. public Expression getTenantId() {
  25. String merchantCode = SystemContext.getUserInfo().getMerchantCode();
  26. if (Objects.isNull(merchantCode)) {
  27. return new StringValue("-1");
  28. } else {
  29. return new StringValue(SystemContext.getUserInfo().getMerchantCode());
  30. }
  31. }
  32. // 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
  33. @Override
  34. public boolean ignoreTable(String tableName) {
  35. return tenantProperties.getExclusionTable().stream().anyMatch(
  36. (t) -> t.equalsIgnoreCase(tableName));
  37. }
  38. /**
  39. * 获取多租户的字段名
  40. * @return String
  41. */
  42. @Override
  43. public String getTenantIdColumn() {
  44. return tenantProperties.getColumn();
  45. }
  46. }));
  47. }
  48. interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
  49. return interceptor;
  50. }
  51. }

是不是很简单;基于管理平台这类的需求,如何忽略多租户呢; 我的建议是新建不同的mapper对象

  1. import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
  2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  3. import com.caicongyang.hc.entity.User;
  4. import org.apache.ibatis.annotations.Param;
  5. /**
  6. * <p>
  7. * 用户表 Mapper 接口
  8. * </p>
  9. *
  10. * @author caicongyang
  11. * @since 2024-01-18
  12. */
  13. @InterceptorIgnore(tenantLine = "true")
  14. public interface UserTenantIngoreMapper extends BaseMapper<User> {
  15. User getUserByMobile(@Param("mobile") String mobile);
  16. }

虽然 @InterceptorIgnore(tenantLine = "true") 注解也支持注在方法上,但是基于不同的mapper 来做是否租户管理,个人建议是比较清晰的;

以上基于官方提供的多租户管理插件还是很方便的; 那接下来看看官方提供的数据权限插件;

2 数据权限插件

通看官方提供的数据权限插件  数据权限插件 | MyBatis-Plus  感觉使用起来没有多租户插件来的清晰和方便; 找了很多网上的例子,很多推荐基于注解等; 在现实的项目中,往往几千个方法,一个个价注解实在不方便; 个人基于多租户插件改造的数据权限可以参考,相对来还是蛮方便的,请君观看;

同样的熟悉配置类:

  1. import lombok.Data;
  2. import org.springframework.boot.context.properties.ConfigurationProperties;
  3. import org.springframework.context.annotation.Configuration;
  4. import java.util.List;
  5. /**
  6. * 白名单配置
  7. */
  8. @Data
  9. @Configuration
  10. @ConfigurationProperties(prefix = "data.permission")
  11. public class DataPermissionProperties {
  12. /**
  13. * 是否开启数据权限模式
  14. */
  15. private Boolean enable = false;
  16. /**
  17. * 需要排除的多租户的表
  18. */
  19. private List<String> exclusionTable;
  20. }

拦截器类:

  1. import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
  2. import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
  3. import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
  4. import com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor;
  5. import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
  6. import java.sql.Connection;
  7. import java.sql.SQLException;
  8. import java.util.List;
  9. import lombok.AllArgsConstructor;
  10. import lombok.Data;
  11. import lombok.EqualsAndHashCode;
  12. import lombok.NoArgsConstructor;
  13. import lombok.ToString;
  14. import net.sf.jsqlparser.expression.Expression;
  15. import net.sf.jsqlparser.schema.Table;
  16. import net.sf.jsqlparser.statement.delete.Delete;
  17. import net.sf.jsqlparser.statement.select.PlainSelect;
  18. import net.sf.jsqlparser.statement.select.Select;
  19. import net.sf.jsqlparser.statement.select.SelectBody;
  20. import net.sf.jsqlparser.statement.select.SetOperationList;
  21. import net.sf.jsqlparser.statement.update.Update;
  22. import org.apache.ibatis.executor.Executor;
  23. import org.apache.ibatis.executor.statement.StatementHandler;
  24. import org.apache.ibatis.mapping.BoundSql;
  25. import org.apache.ibatis.mapping.MappedStatement;
  26. import org.apache.ibatis.mapping.SqlCommandType;
  27. import org.apache.ibatis.session.ResultHandler;
  28. import org.apache.ibatis.session.RowBounds;
  29. @Data
  30. @NoArgsConstructor
  31. @AllArgsConstructor
  32. @ToString(callSuper = true)
  33. @EqualsAndHashCode(callSuper = true)
  34. public class TomDataPermissionInnerInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
  35. private TomDataPermissionHandler dataPermissionHandler;
  36. @Override
  37. public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  38. if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
  39. return;
  40. }
  41. PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
  42. mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
  43. }
  44. @Override
  45. public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
  46. PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
  47. MappedStatement ms = mpSh.mappedStatement();
  48. SqlCommandType sct = ms.getSqlCommandType();
  49. if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
  50. if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
  51. return;
  52. }
  53. PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
  54. mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
  55. }
  56. }
  57. @Override
  58. protected void processSelect(Select select, int index, String sql, Object obj) {
  59. SelectBody selectBody = select.getSelectBody();
  60. if (selectBody instanceof PlainSelect) {
  61. this.setWhere((PlainSelect) selectBody, (String) obj);
  62. } else if (selectBody instanceof SetOperationList) {
  63. SetOperationList setOperationList = (SetOperationList) selectBody;
  64. List<SelectBody> selectBodyList = setOperationList.getSelects();
  65. selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
  66. }
  67. }
  68. /**
  69. * 设置 where 条件
  70. *
  71. * @param plainSelect 查询对象
  72. * @param whereSegment 查询条件片段
  73. */
  74. protected void setWhere(PlainSelect plainSelect, String whereSegment) {
  75. if (dataPermissionHandler instanceof MultiDataPermissionHandler) {
  76. processPlainSelect(plainSelect, whereSegment);
  77. return;
  78. }
  79. // 兼容旧版的数据权限处理
  80. final Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), whereSegment);
  81. if (null != sqlSegment) {
  82. plainSelect.setWhere(sqlSegment);
  83. }
  84. }
  85. /**
  86. * update 语句处理
  87. */
  88. @Override
  89. protected void processUpdate(Update update, int index, String sql, Object obj) {
  90. if (dataPermissionHandler.ignoreTable(update.getTable().getName())) {
  91. return;
  92. }
  93. final Expression sqlSegment = getUpdateOrDeleteExpression(update.getTable(), update.getWhere(), (String) obj);
  94. if (null != sqlSegment) {
  95. update.setWhere(sqlSegment);
  96. }
  97. }
  98. /**
  99. * delete 语句处理
  100. */
  101. @Override
  102. protected void processDelete(Delete delete, int index, String sql, Object obj) {
  103. if (dataPermissionHandler.ignoreTable(delete.getTable().getName())) {
  104. return;
  105. }
  106. final Expression sqlSegment = getUpdateOrDeleteExpression(delete.getTable(), delete.getWhere(), (String) obj);
  107. if (null != sqlSegment) {
  108. delete.setWhere(sqlSegment);
  109. }
  110. }
  111. protected Expression getUpdateOrDeleteExpression(final Table table, final Expression where, final String whereSegment) {
  112. if (dataPermissionHandler instanceof MultiDataPermissionHandler) {
  113. return andExpression(table, where, whereSegment);
  114. } else {
  115. // 兼容旧版的数据权限处理
  116. return dataPermissionHandler.getSqlSegment(where, whereSegment);
  117. }
  118. }
  119. @Override
  120. public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
  121. if (dataPermissionHandler.ignoreTable(table.getName())) {
  122. return null;
  123. }
  124. // 只有新版数据权限处理器才会执行到这里
  125. final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
  126. return handler.getSqlSegment(table, where, whereSegment);
  127. }
  128. }

handler 接口

  1. import net.sf.jsqlparser.expression.Expression;
  2. public interface TomDataPermissionHandler {
  3. /**
  4. * 获取数据权限 SQL 片段
  5. *
  6. * @param where 待执行 SQL Where 条件表达式
  7. * @param mappedStatementId Mybatis MappedStatement Id 根据该参数可以判断具体执行方法
  8. * @return JSqlParser 条件表达式,返回的条件表达式会覆盖原有的条件表达式
  9. */
  10. Expression getSqlSegment(Expression where, String mappedStatementId);
  11. /**
  12. * 根据表名判断是否忽略拼接多租户条件
  13. * <p>
  14. * 默认都要进行解析并拼接多租户条件
  15. *
  16. * @param tableName 表名
  17. * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
  18. */
  19. default boolean ignoreTable(String tableName) {
  20. return false;
  21. }
  22. }

真正的handler 实现类

  1. package com.caicongyang.hc.conf;
  2. import com.caicongyang.cache.constant.CommonCacheConst;
  3. import com.caicongyang.cache.util.RedisUtil;
  4. import com.caicongyang.context.context.SystemContext;
  5. import com.caicongyang.context.context.UserInfo;
  6. import com.caicongyang.orm.mybatis.TomDataPermissionHandler;
  7. import com.caicongyang.orm.mybatis.dto.UserDataAuthorityDTO;
  8. import lombok.extern.slf4j.Slf4j;
  9. import net.sf.jsqlparser.expression.Expression;
  10. import net.sf.jsqlparser.expression.StringValue;
  11. import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
  12. import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
  13. import net.sf.jsqlparser.expression.operators.relational.InExpression;
  14. import net.sf.jsqlparser.expression.operators.relational.ItemsList;
  15. import net.sf.jsqlparser.schema.Column;
  16. import org.apache.commons.collections.CollectionUtils;
  17. import org.apache.commons.lang.StringUtils;
  18. import java.util.Objects;
  19. import java.util.stream.Collectors;
  20. @Slf4j
  21. public class CommonDataPermissionHandler implements TomDataPermissionHandler {
  22. DataPermissionProperties dataPermissionProperties;
  23. public CommonDataPermissionHandler(DataPermissionProperties dataPermissionProperties) {
  24. this.dataPermissionProperties = dataPermissionProperties;
  25. }
  26. public CommonDataPermissionHandler() {
  27. }
  28. @Override
  29. public Expression getSqlSegment(Expression where, String mappedStatementId) {
  30. // 从上下文中取出数据权限
  31. UserInfo userInfo = SystemContext.getUserInfo();
  32. String token = SystemContext.getToken();
  33. UserDataAuthorityDTO dto = RedisUtil.get(String.format(CommonCacheConst.USER_DATA_ROLE_KEY, token), UserDataAuthorityDTO.class);
  34. log.debug("开始进行权限过滤:{} , where: {},mappedStatementId: {}", where, mappedStatementId);
  35. if (userInfo == null || StringUtils.isBlank(userInfo.getUserCode())) {
  36. return where;
  37. }
  38. //组数据权限
  39. if (Objects.nonNull(dto) && CollectionUtils.isNotEmpty(dto.getGroupCodes())) {
  40. ItemsList itemsList = new ExpressionList(dto.getGroupCodes().stream().map(StringValue::new).collect(Collectors.toList()));
  41. InExpression inExpression = new InExpression(new Column("group_code"), itemsList);
  42. return new AndExpression(where, inExpression);
  43. }
  44. // 供应商数据权限
  45. if (Objects.nonNull(dto) && CollectionUtils.isNotEmpty(dto.getSupplierCodes())) {
  46. ItemsList itemsList = new ExpressionList(dto.getSupplierCodes().stream().map(StringValue::new).collect(Collectors.toList()));
  47. InExpression inExpression = new InExpression(new Column("supplier_code"), itemsList);
  48. return new AndExpression(where, inExpression);
  49. }
  50. return where;
  51. }
  52. public boolean ignoreTable(String tableName) {
  53. return dataPermissionProperties.getExclusionTable().stream().anyMatch(
  54. (t) -> t.equalsIgnoreCase(tableName));
  55. }
  56. }

完整的mybatis 配置类如下:

  1. package com.caicongyang.hc.conf;
  2. import com.baomidou.mybatisplus.annotation.DbType;
  3. import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
  4. import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
  5. import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
  6. import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
  7. import com.caicongyang.context.context.SystemContext;
  8. import com.caicongyang.orm.mybatis.BasePOMetaObjectHandler;
  9. import com.caicongyang.orm.mybatis.TenantProperties;
  10. import com.caicongyang.orm.mybatis.TomDataPermissionInnerInterceptor;
  11. import net.sf.jsqlparser.expression.Expression;
  12. import net.sf.jsqlparser.expression.StringValue;
  13. import org.apache.ibatis.plugin.Interceptor;
  14. import org.springframework.beans.factory.annotation.Autowired;
  15. import org.springframework.context.annotation.Bean;
  16. import org.springframework.context.annotation.Configuration;
  17. import java.util.Objects;
  18. @Configuration
  19. public class MybatisPlusConfiguration {
  20. @Autowired
  21. TenantProperties tenantProperties;
  22. @Autowired
  23. DataPermissionProperties dataPermissionProperties;
  24. /**
  25. * 先多租户配置,再数据权限配置,再分页插件;顺序不能乱
  26. * @return
  27. */
  28. @Bean
  29. public Interceptor paginationInnerInterceptor() {
  30. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  31. if (tenantProperties.getEnable()) {
  32. interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
  33. @Override
  34. public Expression getTenantId() {
  35. String merchantCode = SystemContext.getUserInfo().getMerchantCode();
  36. if (Objects.isNull(merchantCode)) {
  37. return new StringValue("-1");
  38. } else {
  39. return new StringValue(SystemContext.getUserInfo().getMerchantCode());
  40. }
  41. }
  42. // 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
  43. @Override
  44. public boolean ignoreTable(String tableName) {
  45. return tenantProperties.getExclusionTable().stream().anyMatch(
  46. (t) -> t.equalsIgnoreCase(tableName));
  47. }
  48. /**
  49. * 获取多租户的字段名
  50. * @return String
  51. */
  52. @Override
  53. public String getTenantIdColumn() {
  54. return tenantProperties.getColumn();
  55. }
  56. }));
  57. }
  58. if (dataPermissionProperties.getEnable()) {
  59. interceptor.addInnerInterceptor(new TomDataPermissionInnerInterceptor(new CommonDataPermissionHandler(dataPermissionProperties)));
  60. }
  61. interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
  62. return interceptor;
  63. }
  64. }

希望大家从我的例子对大家有所帮助;我也是翻看了所有的网上的记录,自己改写的一个demo

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/407486
推荐阅读
相关标签
  

闽ICP备14008679号