公司的业务场景,经常会遇到,限制用户查询数据范围场景,在整理需求,查找资料之后,实现了通过注解+mybatis拦截器,在sql中动态添加权限字段及值的功能。使用的时候方便简单,so,总结一下,写出来以供参考。(公司业务要更复杂一点,当前写的是简化后的,不过原理是一样的)
用户表:
CREATE TABLE `t_basic_user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`com_id` int(10) unsigned DEFAULT '0' COMMENT '创建公司id',
`name` varchar(15) NOT NULL DEFAULT '' COMMENT '人员姓名',
`account` varchar(11) NOT NULL DEFAULT '' COMMENT '人员账号',
`pwd` char(64) NOT NULL DEFAULT '' COMMENT '人员密码',
`dept_id` int(10) unsigned DEFAULT '0' COMMENT '人员部门id',
`role_id` int(10) unsigned DEFAULT '0' COMMENT '人员角色id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10606 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='人员表';
公司表:
CREATE TABLE `t_basic_company` (
`id` int(10) unsigned NOT NULL DEFAULT '0',
`root_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '顶级公司id',
`pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '上级公司id',
`name` varchar(15) NOT NULL DEFAULT '' COMMENT '公司名称',
`level_num` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '公司层级'
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='公司表';
角色表:
CREATE TABLE `t_basic_role_auth` (
`id` int(20) unsigned NOT NULL AUTO_INCREMENT,
`uid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建人id',
`role_id` int(20) unsigned NOT NULL DEFAULT '0' COMMENT '人员角色id',
`auth` varchar(50) NOT NULL DEFAULT '' COMMENT '权限key',
`data` tinyint(1) unsigned DEFAULT '0' COMMENT '权限value',
PRIMARY KEY (`id`) USING BTREE,
KEY `com_id` (`com_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=67681 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
根据登录用户的id,所属部门id,所属单位id,结合当前登录用户的角色权限,决定当前用户能够查看到哪些数据。
例如:用户数据查看权限,权限的auth_key设置为user_list_view,每个角色所对应的角色表中的data,表示不同的数据范围。大致分为以下几种:
1:本单位
2:本单位及下级单位
3:无权限
4:所有数据
需要在访问用户信息列表的时候,根据当前角色在用户信息这个功能的权限下,所对应的data值,返回数据列表。达到不同角色,返回不同范围数据的目的。
1.使用注解,在mapper中将对应的authkey写入,并且标识清楚权限所限制的数据库表名,及字段名。(例:用户信息权限中,需要根据用户的com_id来限制数据,就需要表示清楚数据库名-t_basic_user,字段名-com_id)
2.在拦截器中,获取到当前用户id;根据注解,获取到对应的authkey,;然后再用这两个信息,到角色表中,获取到具体的data值
3.根据data值,设置权限的具体值;如果是本单位权限,则设置com_id 等于当前用户的公司id,如果是本单位及下级单位权限,则设置com_id在本单位及下级单位的公司列表中。
4.根据注解中定义的需要限制的表名和字段名,将限制条件,添加在sql后面
com.baomidou
mybatis-plus-boot-starter
3.4.2
com.baomidou
mybatis-plus-generator
3.4.1
/**
* @author likm
* 数据范围注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataScope {
/**
* 当前功能的authKey,根据authKey去查询当前用户的具体权限值
*/
String authKey() default "";
/**
* 需要加数据权限范围的表别名,单位权限
*/
String comTableName() default "com";
String comFieldName() default "com_id";
}
/**
* 用户权限查询返回结构
*
* @author likm
*/
@Data
public class UserAuth implements Serializable {
/**
* 是否所有数据
*/
private Boolean all;
/**
* 是否无权限
*/
private Boolean none;
/**
* 限制的权限值
*/
private List<Integer> ids;
}
/**
* @author likm
* @date 2021/7/29
* @description 数据范围权限值相关枚举,模板
*/
public enum DataScopeViewTypeEnum {
// 不能查看= 0
//无权限
VIEW_NONE(0, "无权限"),
// 查看所有的= 1
VIEW_ALL(1, "查看所有的"),
// 查看自己的= 2
VIEW_ME(2, "查看自己的"),
// 查看本单位的= 3
VIEW_COMPANY(3, "查看本单位的"),
// 查看本单位及其下级单位的= 4
VIEW_COMPANY_AND_SUB(4, "查看本单位及其下级单位的"),
;
DataScopeViewTypeEnum(Integer code, String name) {
this.code = code;
this.name = name;
}
private Integer code;
private String name;
public Integer getCode() {
return code;
}
public String getName() {
return name;
}
public static DataScopeViewTypeEnum getByValue(Integer value) {
for (DataScopeViewTypeEnum transactType : values()) {
if (transactType.getCode().equals(value)) {
return transactType;
}
}
return null;
}
}
/**
* 数据范围权限需要业务层实现的接口
*
* @Author likm
* @Date 2021/6/24
* @Description //TODO
* @Version 1.0
**/
public interface UserAuthInterface {
/**
* 查询用户对应权限的限制范围值
*
* @param authKey 权限值key
* @return 用户的权限值
*/
UserAuth getUserAuth(String authKey);
}
/**
* @Author likm
* @Date 2021/7/29
* @Description //TODO
* @Version 1.0
**/
@Service
public class UserAuthInterfaceImpl implements UserAuthInterface {
@Autowired
@Lazy //不知道咋回事,在拦截器中自动注入mapper之后,会出现循环依赖,暂时未找到好的解决方式,只能通过懒加载注解来解决
private BasicCompanyMapper companyMapper;
@Override
public UserAuth getUserAuth(String authKey) {
UserAuth userAuth = new UserAuth();
//通过authKey和当前用户userId,获取到authKey对应的权限值data
//StorageUtils.getCurrentUserId() 的实现逻辑,是请求参数中在header中带上token,然后使用拦截器进行解析之后,将用户信息存放在threadLocal中
Integer data = basicUserMapper.getAuthDataByKey(StorageUtils.getCurrentUserId(), authKey);
if (!Objects.isNull(authData)) {
switch (DataScopeViewTypeEnum.getByValue(data)) {
case VIEW_NONE:
userAuth.setNone(Boolean.TRUE);
break;
case VIEW_ALL:
userAuth.setAll(Boolean.TRUE);
break;
case VIEW_COMPANY_AND_SUB:
List<BasicCompany> companyList = companyMapper.selectList(new LambdaQueryWrapper<BasicCompany>()
.eq(BasicCompany::getPid, StorageUtils.getCurrentUserComId()));
List<Integer> comIds = companyList.stream().map(BasicCompany::getId).collect(Collectors.toList());
comIds.add(StorageUtils.getCurrentUserComId());
userAuth.setIds(comIds);
break;
case VIEW_COMPANY:
default:
//StorageUtils.getCurrentUserComId() 获取原理同StorageUtils.getCurrentUserId(),都是存放在threadLocal中的数据
userAuth.setIds(Arrays.asList(StorageUtils.getCurrentUserComId()));
break;
}
}
return userAuth;
}
}
/**
* @Author likm
* @Date 2021/7/12
* @Description //数据范围权限拦截器,配合@DataScope注解使用
* @Version 1.0
**/
public class DataScopeInnerInterceptor implements InnerInterceptor {
@Autowired
private UserAuthInterface authInterface;
@SneakyThrows
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//获取执行方法的位置
String namespace = ms.getId();
//获取mapper名称
String className = namespace.substring(0, namespace.lastIndexOf("."));
//获取方法名
String methodName = namespace.substring(namespace.lastIndexOf(".") + 1);
//获取当前mapper 的方法
Method[] methods = Class.forName(className).getMethods();
for (Method m : methods) {
if (Objects.equals(m.getName(), methodName)) {
//获取注解 来判断是不是要处理sql
DataScope dataScope = m.getAnnotation(DataScope.class);
if (Objects.isNull(dataScope) || Objects.equals("", dataScope.authKey())) {
//没有数据权限相关枚举,直接跳过
continue;
}
UserAuth auth = authInterface.getUserAuth(dataScope.authKey());
//去除特殊字符
String originalSql = boundSql.getSql().replaceAll("\r|\n|`", "");
if (!Objects.isNull(auth)) {
//根据用户权限拼接sql
String newSql = getInExpressionByAuth(auth, dataScope, originalSql);
//通过反射修改sql语句
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, newSql);
}
}
}
}
/**
* 根据权限,拼接SQL
*/
public static String getInExpressionByAuth(UserAuth auth, DataScope dataScope, String sql) {
//如果是admin账户,直接返回null,不修改原sql
if (!Objects.isNull(auth.getAll()) && auth.getAll()) {
return null;
}
//如果是无权限,拼接1=2
if (!Objects.isNull(auth.getNone()) && auth.getNone()) {
return addWhereCondition(sql, "1=2");
}
return addWhereCondition(sql, getCondition(dataScope.comTableName(), dataScope.comFieldName(), auth.getIds()));
}
/**
* 生成where 条件字符串
*
* @param tableName 表名
* @param fieldName 字段名
* @param ids 值
* @return
*/
private static String getCondition(String tableName, String fieldName, List<Integer> ids) {
return tableName + "." + fieldName + " in (" + StringUtils.join(ids, ",") + ")";
}
/**
* 在原有的sql中增加新的where条件
*
* @param sql 原sql
* @param condition 新的and条件
* @return 新的sql
*/
public static String addWhereCondition(String sql, String condition) {
try {
Select select = (Select) CCJSqlParserUtil.parse(sql);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
final Expression expression = plainSelect.getWhere();
final Expression envCondition = CCJSqlParserUtil.parseCondExpression(condition);
if (Objects.isNull(expression)) {
plainSelect.setWhere(envCondition);
} else {
AndExpression andExpression = new AndExpression(expression, envCondition);
plainSelect.setWhere(andExpression);
}
return plainSelect.toString();
} catch (JSQLParserException e) {
throw new RuntimeException(e);
}
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 由于拦截器先于spring context注入,所以在拦截器中无法自动注入接口,
* 需要先在此处将拦截器注入
*/
@Bean
public DataScopeInnerInterceptor dataScopeInnerInterceptor() {
return new DataScopeInnerInterceptor();
}
/**
* mybatis拦截器注入,
* 可以将其他自定义拦截器从这里注入,方便在分页之前执行
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
System.out.println("注入userAuthInterface");
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(dataScopeInnerInterceptor());
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
数据范围权限的使用方式,是在mapper中使用@DataScope注解,在注解中,设置当前sql,所相关权限的authKey值,以及数据范围权限,所限制的表名
@DataScope(authKey = AuthKeyConstant.ILLEGAL_BUILD_DAYLIY_INSPECT_VIEW, comTableName = "u")
Page<IllegalBuildingPatrolVO> getList(Page page, @Param("dto") IllegalBuildingPatrolDTO dto);
最终sql样式如下:
1.本单位
SELECT
p.*,
u.name userName
FROM
t_illegal_building_patrol p
LEFT JOIN t_basic_user u ON u.id = p.create_uid
WHERE
p.is_del = 0
AND u.com_id IN ( 1 )
ORDER BY
p.id DESC
2.本单位及下级单位
SELECT
p.*,
u.name userName
FROM
t_illegal_building_patrol p
LEFT JOIN t_basic_user u ON u.id = p.create_uid
WHERE
p.is_del = 0
AND u.com_id IN ( 1, 2, 3 )
ORDER BY
p.id DESC
以上,数据权限功能就完成了,本身我们公司的业务还涉及到部门权限、人员权限等相关限制,逻辑更复杂一些。不过基本逻辑一致
参考:
[1]: https://blog.csdn.net/sundasheng44/article/details/108118094