Springboot 通过自定义注解+切面 实现sql查询的数据权限控制
2024-07-27 14:53 阅读(324)

前言

在项目中,存在一种权限形式,叫做数据权限,它不同于常见的菜单权限、按钮权限等,它主要是对数据进行过滤,比如,当查询人员信息表时,应当只返回当前单位的人员,而不应该把所有的人员数据都返回。简单的数据权限 我理解为就是在查询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完成了简单的数据过滤,具体实现细节可视业务而定。