前言
在项目中,存在一种权限形式,叫做数据权限,它不同于常见的菜单权限、按钮权限等,它主要是对数据进行过滤,比如,当查询人员信息表时,应当只返回当前单位的人员,而不应该把所有的人员数据都返回。简单的数据权限 我理解为就是在查询sql后面追加一些额外条件,例如在查询时,获取到当前用户的单位Id 拼接到sql中,但是如果每个sql都手动修改,未免太过繁琐,最近我学习了通过自定义注解+切面的方式,横向处理数据权限问题,本文就来分享一下。
场景
为了演示效果,这里定义一个简单场景:用户表中存在一个部门id字段,当某用户查询用户数据时,应仅查询出和他同部门下的人员数据。
下面是一个简单的表结构:
插入几条测试数据
根据我们的需求,当张三登录系统,应该只能查到张三李四,李四同理。王五登录系统时应该只能查询到自己。功能很好实现,假如查询sql为:select * from t_user where name = '',追加数据权限其实就是从后面追加and dept_id = {查询用户部门id}而已,下面分享一下实现细节。
代码
创建一个简单的示例项目,引入一些依赖,这里引入了SpringSecurity,直接使用它提供的登录功能,另外引入了mybatis-plus、mysql驱动等。
<dependencies>
<!--SpringSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
提供两个简单的查询接口:模拟了平时开发中常见的查询方式,将请求参数封装到Map中、以及使用对象作为参数查询。
controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/listByMap")
public List<UserEntity> listByMap(Integer id, String name) {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", name);
return userService.listByMap(map);
}
@GetMapping("/listByEntity")
public List<UserEntity> listByEntity(Integer id, String name) {
UserEntity entity = new UserEntity();
entity.setId(id);
entity.setName(name);
return userService.listByEntity(entity);
}
}
service
public interface UserService {
List<UserEntity> listByMap(Map<String, Object> map);
List<UserEntity> listByEntity(UserEntity entity);
UserEntity loadUserByUsername(String username);
}
@Service
public class UserServiceImpl implements UserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public List<UserEntity> listByMap(Map<String, Object> map) {
return userMapper.listByMap(map);
}
@Override
public List<UserEntity> listByEntity(UserEntity entity) {
return userMapper.listByEntity(entity);
}
@Override
public UserEntity loadUserByUsername(String username) {
return userMapper.loadUserByUsername(username);
}
}
mapper
@Mapper
public interface UserMapper {
List<UserEntity> listByMap(@Param("param") Map<String, Object> map);
List<UserEntity> listByEntity(@Param("user") UserEntity entity);
}
<mapper namespace="com.example.dataauthdemo.mapper.UserMapper">
<select id="listByMap" parameterType="Map" resultType="com.example.dataauthdemo.entity.UserEntity">
select * from t_user t
<where>
<if test="param.id != null and param.id != ''">
t.id = #{param.id}
</if>
<if test="param.name != null and param.name != ''">
t.name LIKE CONCAT('%',#{param.name},'%')
</if>
</where>
</select>
<select id="listByEntity" parameterType="com.example.dataauthdemo.entity.UserEntity" resultType="com.example.dataauthdemo.entity.UserEntity">
select * from t_user t
<where>
<if test="user.id != null and user.id != ''">
t.id = #{user.id}
</if>
<if test="user.name != null and user.name != ''">
t.name LIKE CONCAT('%',#{user.name},'%')
</if>
</where>
</select>
</mapper>
entity
@Data
public class UserEntity implements UserDetails {
private Integer id;
private String username;
private String password;
private String name;
private Integer deptId;
//省略get set
}
以上代码部分细节与SpringSecurity有关,读者可自行了解。
实现
根据上面的代码不难看出,当进行sql查询的时候,参数要么是封装为了Map集合,要么是UserEntity对象。那如果想实现数据过滤,直接处理入参对象即可,对应sql简单修改即可。
定义一个自定义注解@DeptScope,用来标注在service层方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeptScope {
/**
* 需要过滤数据的表的别名 默认为t 过滤时封装为t.dept_id = ''
* @return
*/
String tableAlias() default "t";
}
定义一个切面,用来拦截@DeptScope
@Aspect
@Component
public class DeptScopeAspect {
@Before("@annotation(deptScope)")
public void doBefore(JoinPoint joinPoint, DeptScope deptScope) {
//获取当前登录用户部门id
UserEntity user = (UserEntity) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Integer deptId = user.getDeptId();
//获取方法入参
Object arg = joinPoint.getArgs()[0];
//判断参数类型,分别处理
if (arg instanceof Map) {
Map<String, Object> paramsMap = (Map<String, Object>) arg;
paramsMap.put("deptScope", " AND (" + deptScope.tableAlias() + ".dept_id = '" + deptId + "')");
} else {
try {
//获取属性对象和方法对象
Class<?> aClass = arg.getClass();
Field field = aClass.getDeclaredField("deptId");
Method getDeptId = aClass.getMethod("getDeptId");
Method setDeptId = aClass.getMethod("setDeptId", field.getType());
//获取该属性的值 如果和当前部门id不一致,说明发生了越权访问 设置一个不可能查询出数据的值,反之设置当前部门
Object fieldValue = getDeptId.invoke(arg);
boolean unauthorized = fieldValue != null && !fieldValue.toString().equals(deptId);
setDeptId.invoke(arg, unauthorized ? deptId + "-" + fieldValue : deptId);
} catch (Exception e) {
//抛出异常说明入参类型中没有deptId这个属性,不用做处理
}
}
}
}
在service层对象方法上加上注解
@Override
@DeptScope
public List<UserEntity> listByMap(Map<String, Object> map) {
return userMapper.listByMap(map);
}
@Override
@DeptScope
public List<UserEntity> listByEntity(UserEntity entity) {
return userMapper.listByEntity(entity);
}
微调xml文件
<mapper namespace="com.example.dataauthdemo.mapper.UserMapper">
<select id="listByMap" parameterType="Map" resultType="com.example.dataauthdemo.entity.UserEntity">
select * from t_user t
<where>
<if test="param.id != null and param.id != ''">
t.id = #{param.id}
</if>
<if test="param.name != null and param.name != ''">
t.name LIKE CONCAT('%',#{param.name},'%')
</if>
${param.deptScope}
</where>
</select>
<select id="listByEntity" parameterType="com.example.dataauthdemo.entity.UserEntity" resultType="com.example.dataauthdemo.entity.UserEntity">
select * from t_user t
<where>
<if test="user.id != null and user.id != ''">
t.id = #{user.id}
</if>
<if test="user.name != null and user.name != ''">
t.name LIKE CONCAT('%',#{user.name},'%')
</if>
<if test="user.deptId != null and user.deptId != ''">
t.dept_id = #{user.deptId}
</if>
</where>
</select>
<select id="loadUserByUsername" resultType="com.example.dataauthdemo.entity.UserEntity">
select * from t_user where username = #{username} limit 1
</select>
</mapper>
总结
以上,就通过AOP完成了简单的数据过滤,具体实现细节可视业务而定。