SaToken使用SpringBoot整合SaToken关于数据权限

Posted 符华-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SaToken使用SpringBoot整合SaToken关于数据权限相关的知识,希望对你有一定的参考价值。

目录


点击查看上一篇: 【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解鉴权

前言

数据范围:1 所有数据、2 所在部门及子部门数据、3 所在部门数据、4 仅本人数据、5 自定义数据


关于数据权限,一直没有一个很好的通用的解决方案。


1、 之前有试过参考若依的那个用自定义注解+aop去实现数据权限,然后觉得通用性不高,无法适用于我自己的项目的业务场景,而且我用的mybatisplus,不是联表的话是不会去xml自己写sql语句的,而是直接用QueryWrapper,用这种实现方式是没办法把拼接好的sql放到QueryWrapper中做条件的。


2、 然后我又找了种方法,也是用注解和aop,但它是需要解析sql,把数据范围条件拼接到sql中,变成了一条新的sql语句去执行的。尝试了下这种方法,确实可以,不管是在xml自己写的SQL也好,还是用户mybatisplus自带的查询也好,甚至有分页都不用担心。但是面对复杂SQL比如子查询、嵌套查询等,要想把条件给拼接到正确的位置上去,太复杂了,那些group by、where、order by这些关键词位置判断就够呛,要是有子查询、嵌套查询,都有where,那过滤条件放到哪个where里面?这也是比较头疼的事。所以我感觉这种方法也不太行,代码太啰嗦。


后面做项目的时候,我上面两种方法都没采用,我的想法是两种方法最后都是在原有的SQL上拼接条件,那为什么不把权限条件像其他条件一样,直接写在SQL里面,而是用注解、拦截器去拼接?就比如查询用户列表,a用户的数据权限是只能查看本部门的数据,所以权限条件是 dept_id=a用户的部门 ,然后列表有个搜索框,搜索姓名的,那么在mybatis中SQL就是

select *
from sys_user
<where>
	<if test="name != null"> and name=#name </if>
	<if test="dataScope != null"> and $dataScope </if>
</where>

如果不是自己写SQL,而是用QueryWrapper的话,那也简单

public PageInfo<SysUser> page(SysQuery query)
    QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
    if (StrUtil.isNotBlank(query.getName()))
        queryWrapper.eq("name",query.getName());
    
    String apply = queryApplyScope("dept",null,1); // 获取权限条件
    if (StrUtil.isNotBlank(apply))
        queryWrapper.apply(apply);
    
    return new PageInfo<>(baseMapper.selectList(queryWrapper));

像这样把权限条件和其他筛选条件一样,直接用不是挺好的嘛?还是说为了做到低侵入、无侵入?不管哪种方法,得看是否适用当前业务场景,不过如果没要求无侵入的话,直接用肯定是比注解+拦截器拼接的方式更灵活的。下面详细说下第二种实现和第三种实现,可以参考下。

一、自定义注解+拦截器拦截SQL,实现改变原SQL


参考文章:springboot自定义注解+mybatis拦截器实现数据权限

1、自定义注解 DataScope

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope 
    /** 拼接的条件是取 部门id 还是 部门name(1 id 2 name) */
    int type() default 1;
    
    /** 查询的字段名 */
    String fieldName() default "";
    
    /** 查询本人数据时使用的的字段名 */
    String createBy() default "";

2、数据过滤处理 DataScopeAspect

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.entity.sys.SysUser;
import com.service.sys.SysUserService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;

/**
 * 数据过滤处理
 */
@Aspect
@Component
public class DataScopeAspect 
	/** 用于存储过滤条件的SQL */
    public static final ThreadLocal DATA_AUTH_THREAD_LOCAL = new ThreadLocal();

    @Resource
    private SysUserService sysUserService;

    // 配置织入点
    @Pointcut("@annotation(DataScope)")
    public void dataScopePointCut()  

    @Before("dataScopePointCut()")
    public void doBefore(JoinPoint point)
        DATA_AUTH_THREAD_LOCAL.remove();
        handleDataAuthScope(point);
    

    @After("@annotation(DataScope)")
    public void doAfter()
        //清空数据权限拼接SQL
        DATA_AUTH_THREAD_LOCAL.remove();
    

    /**
     * 处理数据权限
     * @param joinPoint     切面
     */
    protected void handleDataAuthScope(final JoinPoint joinPoint) 
        // 获取当前的用户
        SysUser user = (SysUser) StpUtil.getSession().get("user");
        if (null == user) 
            return;
        
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        DataScope dataScope = method.getAnnotation(DataScope.class);
        // 获取当前用户要拼接的数据权限条件
        String str = sysUserService.queryApplyScope(dataScope.fieldName(), dataScope.createBy(), dataScope.type());
        if (StrUtil.isNotBlank(str))
            //设置数据权限拼接sql
            DATA_AUTH_THREAD_LOCAL.set(str);
        
    

3、拦截SQL语句,并将过滤条件拼接到原SQL中

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;

/**
 * 拦截SQL语句,根据处理好的数据权限条件拼接在原SQL后,组成新的SQL语句
 */
@Component
@Intercepts(@Signature(type = Executor.class, method = "query", args = MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class))
public class DataScopeInterceptor implements Interceptor 

    /** 分组 */
    private static final String GROUP_BY = "GROUP BY";
    /** 排序 */
    private static final String ORDER_BY = "ORDER BY";
    /** 分页 */
    private static final String LIMIT = "LIMIT";
    /** where */
    private static final String WHERE = "WHERE";
    /** where条件 */
    private static final String WHERE_CONDITION = " WHERE 1=1 ";

    /**
     * 拦截sql
     * @param invocation
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable 
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        if (SqlCommandType.SELECT == sqlCommandType) 
            // 获取过滤条件
            Object dataAuthSql = DataScopeAspect.DATA_AUTH_THREAD_LOCAL.get();
            //如果添加数据权限
            if (null != dataAuthSql) 
                BoundSql boundSql = (BoundSql) invocation.getArgs()[5];
                StringBuilder newSqlBuilder = new StringBuilder();
                // 获取到原始sql语句
                String mSql = boundSql.getSql();
                System.out.println("原始SQL:\\t"+mSql);
                mSql = addWhere(mSql);
                //重写sql语句 前面拼接数据权限语句
                if (mSql.indexOf(GROUP_BY) > 0) 
                    newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(GROUP_BY)))
                            .append(" and ")
                            .append(dataAuthSql.toString())
                            .append(" ")
                            .append(mSql.substring(mSql.lastIndexOf(GROUP_BY), mSql.length()));
                 else if (mSql.indexOf(ORDER_BY) > 0) 
                    newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(ORDER_BY)))
                            .append(" and ")
                            .append(dataAuthSql.toString())
                            .append(" ")
                            .append(mSql.substring(mSql.lastIndexOf(ORDER_BY), mSql.length()));
                 else if (mSql.indexOf(LIMIT) > 0) 
                    newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(LIMIT)))
                            .append(" and ")
                            .append(dataAuthSql.toString())
                            .append(" ")
                            .append(mSql.substring(mSql.lastIndexOf(LIMIT), mSql.length()));
                 else if (mSql.indexOf(WHERE) > 0) 
                    newSqlBuilder.append(mSql)
                            .append(" and ")
                            .append(dataAuthSql.toString());
                 else 
                    newSqlBuilder.append(mSql)
                            .append(WHERE_CONDITION)
                            .append(" and ")
                            .append(dataAuthSql.toString());
                
                System.out.println("增强后的SQL:\\t"+newSqlBuilder.toString());
                //通过反射修改sql语句
                Field field = boundSql.getClass().getDeclaredField("sql");
                field.setAccessible(true);
                field.set(boundSql, newSqlBuilder.toString());
            
        
        return invocation.proceed();
    

    /**
     * 添加where关键字
     * @param sql sql语句
     * @return sql
     */
    private String addWhere(String sql) 
        if (sql.indexOf(WHERE) >= 0) 
            return sql;
        
        StringBuilder newSqlBuilder = new StringBuilder();
        if (sql.indexOf(GROUP_BY) > 0) 
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(GROUP_BY)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(GROUP_BY), sql.length()));
         else if (sql.indexOf(ORDER_BY) > 0) 
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(ORDER_BY)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(ORDER_BY), sql.length()));
         else if (sql.indexOf(LIMIT) > 0) 
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(LIMIT)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(LIMIT), sql.length()));
         else 
            newSqlBuilder.append(sql).append(" ").append(WHERE_CONDITION);
        
        return newSqlBuilder.toString();
    

4、用户列表

/**
 * 用户列表
 */
@GetMapping("/list")
@DataScope(fieldName="b.id",createBy="a.id")
@SaCheckPermission("system:user:view")
public ResultVo list(SysQuery query) 
    return ResultUtil.success(service.page(query));

<select id="page" resultType="com.entity.sys.SysUser">
    SELECT a.id,user_name,real_name,dept_id,name deptName
    FROM sys_user a
    LEFT JOIN sys_dept b on a.dept_id=b.id
    <where>
        <if test="code != null">
            AND a.user_name like concat('%', #code, '%')
        </if>
        <if test="name != null">
            AND a.real_name like concat('%', #name, '%')
        </if>
        <if test="parentId != null">
            AND b.id=#parentId
        </if>
    </where>
    ORDER BY a.create_time desc
</select>

以上方式亲测单表、联表查询没什么问题,也不影响分页,但是遇上子查询、嵌套查询就不好使了。

二、关于数据权限条件设置和第三种实现

关于我上面说的第二种、第三种过滤方式,都是要事先设置好拼接条件,用的时候直

以上是关于SaToken使用SpringBoot整合SaToken关于数据权限的主要内容,如果未能解决你的问题,请参考以下文章

SaToken使用SpringBoot整合SaToken关于数据权限

SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权

SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权

SaToken使用SpringBoot整合SaTokentoken自动续期+token定期刷新+注解鉴权

SaToken使用springboot+redis+satoken权限认证

SaToken使用springboot+redis+satoken权限认证