目录
2. 过滤类型枚举 (FilterWhereTypeEnum.java)
3. 权限处理服务 (PermissionHandling.java)
4. Redis配置类 (RedisConfig.java)
5. MyBatis Plus配置 (MybatisPlusConfig.java)
6. 权限拦截器配置 (PermissionRunner.java)
1. 数据权限注解 (DataScope.java)
package com.fantaibao.permission.annotation;
import com.fantaibao.permission.enums.FilterWhereTypeEnum;
import java.lang.annotation.*;
/**
* 数据权限过滤注解
*
* @author fantai
* @date 2023/07/01
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/**
* 表别名
*/
String tableAlias() default "";
/**
* 表字段名
*/
String tableField() default "user_id";
/**
* 过滤类型
*/
FilterWhereTypeEnum type() default FilterWhereTypeEnum.USER_FILTER;
/**
* 是否启用数据权限过滤
*/
boolean enabled() default true;
}
2. 过滤类型枚举 (FilterWhereTypeEnum.java)
package com.fantaibao.permission.enums;
/**
* 数据权限过滤类型枚举
*
* @author fantai
* @date 2023/07/01
*/
public enum FilterWhereTypeEnum {
/**
* 用户过滤 - 基于用户ID
*/
USER_FILTER,
/**
* 门店过滤 - 基于门店ID
*/
STORE_FILTER,
/**
* 部门过滤 - 基于部门ID
*/
DEPT_FILTER,
/**
* 角色过滤 - 基于角色ID
*/
ROLE_FILTER
}
3. 权限处理服务 (PermissionHandling.java)
package com.fantaibao.permission.handling;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 权限处理服务 - 使用Redis缓存权限数据
*
* @author fantai
* @date 2023/07/01
*/
@Service
public class PermissionHandling {
private static final String USER_PERMISSION_KEY_PREFIX = "user:permissions:";
private static final long CACHE_EXPIRE_HOURS = 1;
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 假设有用户服务或门店服务来获取实际数据
// @Resource
// private UserService userService;
//
// @Resource
// private StoreService storeService;
/**
* 根据用户ID获取有权限的用户ID列表
*
* @param userId 用户ID
* @return 有权限的用户ID列表
*/
public List<String> getUserIdsByUserId(String userId) {
String cacheKey = USER_PERMISSION_KEY_PREFIX + "userIds:" + userId;
// 先从Redis获取
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
List<String> userIds = (List<String>) ops.get(cacheKey);
if (!CollectionUtils.isEmpty(userIds)) {
return userIds;
}
// Redis中没有,从数据库获取
// userIds = userService.getPermissionUserIds(userId);
// 这里使用模拟数据
userIds = List.of("1001", "1002", "1003");
// 存入Redis,设置过期时间
if (!CollectionUtils.isEmpty(userIds)) {
ops.set(cacheKey, userIds, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
}
return userIds != null ? userIds : Collections.emptyList();
}
/**
* 根据用户ID获取有权限的门店ID列表
*
* @param userId 用户ID
* @return 有权限的门店ID列表
*/
public List<String> getStoreIdsByUserId(String userId) {
String cacheKey = USER_PERMISSION_KEY_PREFIX + "storeIds:" + userId;
// 先从Redis获取
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
List<String> storeIds = (List<String>) ops.get(cacheKey);
if (!CollectionUtils.isEmpty(storeIds)) {
return storeIds;
}
// Redis中没有,从数据库获取
// storeIds = storeService.getPermissionStoreIds(userId);
// 这里使用模拟数据
storeIds = List.of("2001", "2002", "2003");
// 存入Redis,设置过期时间
if (!CollectionUtils.isEmpty(storeIds)) {
ops.set(cacheKey, storeIds, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
}
return storeIds != null ? storeIds : Collections.emptyList();
}
/**
* 根据用户ID获取有权限的部门ID列表
*
* @param userId 用户ID
* @return 有权限的部门ID列表
*/
public List<String> getDeptIdsByUserId(String userId) {
String cacheKey = USER_PERMISSION_KEY_PREFIX + "deptIds:" + userId;
// 先从Redis获取
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
List<String> deptIds = (List<String>) ops.get(cacheKey);
if (!CollectionUtils.isEmpty(deptIds)) {
return deptIds;
}
// Redis中没有,从数据库获取
// deptIds = deptService.getPermissionDeptIds(userId);
// 这里使用模拟数据
deptIds = List.of("3001", "3002", "3003");
// 存入Redis,设置过期时间
if (!CollectionUtils.isEmpty(deptIds)) {
ops.set(cacheKey, deptIds, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
}
return deptIds != null ? deptIds : Collections.emptyList();
}
/**
* 清除用户权限缓存
*
* @param userId 用户ID
*/
public void clearUserPermissionCache(String userId) {
String userKey = USER_PERMISSION_KEY_PREFIX + "userIds:" + userId;
String storeKey = USER_PERMISSION_KEY_PREFIX + "storeIds:" + userId;
String deptKey = USER_PERMISSION_KEY_PREFIX + "deptIds:" + userId;
redisTemplate.delete(userKey);
redisTemplate.delete(storeKey);
redisTemplate.delete(deptKey);
}
}
4. Redis配置类 (RedisConfig.java)
package com.fantaibao.permission.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类
*
* @author fantai
* @date 2023/07/01
*/
@Configuration
public class RedisConfig {
/**
* 配置Redis模板
*
* @param factory Redis连接工厂
* @return Redis模板
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用StringRedisSerializer来序列化和反序列化redis的key
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 使用GenericJackson2JsonRedisSerializer来序列化和反序列化redis的value
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
5. MyBatis Plus配置 (MybatisPlusConfig.java)
package com.fantaibao.permission.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis Plus配置类
*
* @author fantai
* @date 2023/07/01
*/
@Configuration
public class MybatisPlusConfig {
/**
* 配置MyBatis Plus拦截器
*
* @return MyBatis Plus拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
6. 权限拦截器配置 (PermissionRunner.java)
package com.fantaibao.permission.config;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.fantaibao.permission.annotation.DataScope;
import com.fantaibao.permission.enums.FilterWhereTypeEnum;
import com.fantaibao.permission.handling.PermissionHandling;
import jnpf.base.UserInfo;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 权限拦截器配置 - 应用启动时初始化数据权限拦截器
*
* @author fantai
* @date 2023/07/01
*/
@Component
public class PermissionRunner implements ApplicationRunner {
@Resource
private MybatisPlusInterceptor mybatisPlusInterceptor;
@Resource
private PermissionHandling permissionHandling;
/**
* 应用启动时执行,添加数据权限拦截器
*
* @param args 应用启动参数
*/
@Override
public void run(ApplicationArguments args) {
List<InnerInterceptor> innerInterceptors = new ArrayList<>(mybatisPlusInterceptor.getInterceptors());
innerInterceptors.add(0, new DataPermissionInterceptor(new InnerDataPermissionHandler(permissionHandling)));
mybatisPlusInterceptor.setInterceptors(innerInterceptors);
}
/**
* 内部数据权限处理器
*/
@RequiredArgsConstructor
public static class InnerDataPermissionHandler implements MultiDataPermissionHandler {
private final PermissionHandling permissionHandling;
/**
* 获取SQL片段,用于数据权限过滤
*
* @param table 表信息
* @param where 原始WHERE条件
* @param mappedStatementId Mapper语句ID
* @return 数据权限过滤条件
*/
@Override
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
try {
Class<?> mapperClazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
// 获取Mapper类中声明的方法
Method[] methods = mapperClazz.getDeclaredMethods();
if (methods.length == 0) {
return null;
}
// 查找目标方法
Method targetMethod = Arrays.stream(methods)
.filter(method -> method.getName().equals(methodName))
.findFirst()
.orElse(null);
if (targetMethod == null) {
return null;
}
// 检查方法是否包含DataScope注解
DataScope dataScopeAnnotation = targetMethod.getAnnotation(DataScope.class);
if (ObjectUtils.isEmpty(dataScopeAnnotation) || !dataScopeAnnotation.enabled()) {
return null;
}
// 跳过JOIN中的ON条件表达式拼装
if (isJoinOnCondition(where)) {
return null;
}
// 构建数据权限过滤条件
return buildDataScopeByAnnotation(dataScopeAnnotation);
} catch (Exception e) {
throw new RuntimeException("数据权限处理失败: " + e.getMessage(), e);
}
}
/**
* 判断是否为JOIN ON条件
*
* @param where WHERE条件表达式
* @return 是否为JOIN ON条件
*/
private boolean isJoinOnCondition(Expression where) {
if (where == null) {
return false;
}
// 处理AND表达式的情况
if (where.getASTNode() == null && where instanceof AndExpression) {
Expression leftExpression = ((AndExpression) where).getLeftExpression();
if (leftExpression.getASTNode() != null &&
"RegularCondition".equals(leftExpression.getASTNode().toString()) &&
"JoinerExpression".equals(leftExpression.getASTNode().jjtGetParent().jjtGetParent().toString())) {
return true;
}
}
// 处理普通表达式的情况
if (where.getASTNode() != null &&
"JoinerExpression".equals(where.getASTNode().jjtGetParent().jjtGetParent().toString())) {
return true;
}
return false;
}
/**
* 根据注解构建数据权限过滤表达式
*
* @param dataScopeAnnotation DataScope注解
* @return 数据权限过滤表达式
*/
private Expression buildDataScopeByAnnotation(DataScope dataScopeAnnotation) {
UserInfo userInfo = UserProvider.getUser();
// 管理员拥有所有权限,不需要过滤
if (userInfo.getIsAdministrator()) {
return null;
}
// 获取权限ID列表
List<String> ids = getPermissionIds(dataScopeAnnotation.type(), userInfo.getUserId());
// 权限适用范围为全部或为空时,不需要过滤
if (ids == null || ids.isEmpty()) {
return null;
}
// 构建IN表达式
InExpression inExpression = new InExpression();
ExpressionList expressionList = new ExpressionList();
// 添加权限值到表达式列表
ids.forEach(id -> expressionList.addExpressions(new StringValue(id)));
// 设置字段表达式和值列表
inExpression.setLeftExpression(buildColumn(dataScopeAnnotation.tableAlias(), dataScopeAnnotation.tableField()));
inExpression.setRightItemsList(expressionList);
return inExpression;
}
/**
* 根据过滤类型获取权限ID列表
*
* @param type 过滤类型
* @param userId 用户ID
* @return 权限ID列表
*/
private List<String> getPermissionIds(FilterWhereTypeEnum type, String userId) {
switch (type) {
case USER_FILTER:
return permissionHandling.getUserIdsByUserId(userId);
case STORE_FILTER:
return permissionHandling.getStoreIdsByUserId(userId);
case DEPT_FILTER:
return permissionHandling.getDeptIdsByUserId(userId);
default:
return Collections.emptyList();
}
}
/**
* 构建字段表达式
*
* @param tableAlias 表别名
* @param columnName 字段名称
* @return 字段表达式
*/
private Column buildColumn(String tableAlias, String columnName) {
if (StringUtils.isNotEmpty(tableAlias)) {
columnName = tableAlias + "." + columnName;
}
return new Column(columnName);
}
}
}
7. 使用示例 (TeachingMapper.java)
package com.fantaibao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fantaibao.permission.annotation.DataScope;
import com.fantaibao.permission.enums.FilterWhereTypeEnum;
import com.fantaibao.entity.TeachingRecord;
import com.fantaibao.vo.RecordPageListVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 教学记录Mapper
*
* @author fantai
* @date 2023/07/01
*/
@Mapper
public interface TeachingMapper extends BaseMapper<TeachingRecord> {
/**
* 分页查询教学记录列表 - 基于用户权限过滤
*
* @param pageDto 查询条件
* @return 教学记录列表
*/
@DataScope(tableAlias = "fctr", tableField = "F_UserId", type = FilterWhereTypeEnum.USER_FILTER)
List<RecordPageListVo> recordPageList(@Param("pageDto") TeachingBaseFilter pageDto);
/**
* 分页查询教学记录列表 - 基于门店权限过滤
*
* @param pageDto 查询条件
* @return 教学记录列表
*/
@DataScope(tableAlias = "fctr", tableField = "F_StoreId", type = FilterWhereTypeEnum.STORE_FILTER)
List<RecordPageListVo> recordPageListByStore(@Param("pageDto") TeachingBaseFilter pageDto);
/**
* 分页查询教学记录列表 - 基于部门权限过滤
*
* @param pageDto 查询条件
* @return 教学记录列表
*/
@DataScope(tableAlias = "fctr", tableField = "F_DeptId", type = FilterWhereTypeEnum.DEPT_FILTER)
List<RecordPageListVo> recordPageListByDept(@Param("pageDto") TeachingBaseFilter pageDto);
}
8. 配置文件 (application.yml)
spring:
redis:
host: localhost
port: 6379
database: 0
password:
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
# MyBatis Plus配置
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.fantaibao.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
系统特点
高性能:使用Redis缓存权限数据,减少数据库查询压力
灵活性:支持多种过滤类型(用户、门店、部门等),可根据业务需求灵活配置
安全性:使用MyBatis Plus的数据权限拦截器和JSqlParser,避免SQL注入风险
易用性:通过注解方式配置,代码清晰易懂,便于维护
可扩展性:系统设计支持多种权限过滤类型,可根据业务需求轻松扩展
使用说明
添加注解:在Mapper接口的方法上添加
@DataScope
注解,指定表别名、字段名和过滤类型权限初始化:系统启动时通过ApplicationRunner配置MyBatis Plus的数据权限拦截器
权限缓存:权限数据会自动缓存到Redis,减少数据库查询压力
SQL修改:MyBatis Plus拦截器会自动修改SQL,添加数据权限过滤条件
扩展建议
权限变更通知:实现权限变更时的缓存清除机制,确保数据一致性
多级缓存:可以添加本地缓存作为Redis缓存的前置缓存,进一步提高性能
权限管理界面:开发权限管理界面,动态配置用户数据权限
性能监控:添加权限过滤的性能监控日志,优化权限查询逻辑
这个实现基于MyBatis Plus的数据权限拦截器和Redis缓存,提供了高效、安全的数据权限过滤解决方案,适用于企业级应用的多租户、数据隔离等场景。