Shiro实现多级权限的分页查询

✨重磅!盹猫的个人小站正式上线啦~诚邀各位技术大佬前来探秘!✨

这里有:

  • 硬核技术干货:编程技巧、开发经验、踩坑指南,带你解锁技术新姿势!
  • 趣味开发日常:代码背后的脑洞故事、工具测评,让技术圈不再枯燥~
  • 独家资源分享:开源项目、学习资料包,助你打怪升级快人一步!

👉 点击直达→ 盹猫猫的个人小站 👈

🌟 来逛逛吧,说不定能挖到你正在找的技术宝藏哦~

目录

一、简介

二、分析

1.代码内设置条件

2.方法切面


欢迎来到 盹猫(>^ω^<)的博客

本篇文章主要介绍了

**Shiro实现多级权限的分页查询**

❤博主广交技术好友,喜欢文章的可以关注一下❤

一、简介

当我们使用SpringBoot配合Shiro完成权限验证时,我们知道需要通过在接口(Controller)上添加**@RequiresPermissions("sys:user:page")**注解实现来实现用户具有某个接口权限的访问证明,这中注解在实际应用中只能控制用户访问接口的权限,却不能控制用户访问的数据,如:管理员拥有上述权限,下级的管理员(镇或街道)管理员拥有查看管辖街道(镇)内的权限。这时,只使用该注解是无法知道用户可以访问的是全部还是部分数据了,本文就是记录如何在这种情况下细分用户权限的。

二、分析

1.代码内设置条件

其实很容易想到,我们需要在shiro认证时获取用户用户所在的管辖权限(街道ID、镇街ID),并通过在分页或列表接口中增加镇街ID、街道ID数据过滤条件,来保证用户只能查看自己管辖范围内的数据。

java 复制代码
    public Result<PageData<ApplyDTO>> page(@ApiIgnore @RequestParam Map<String, Object> params) {
        //TODO  对应区域权限只能查看区域内的数据
        params.put("streetId", SecurityUser.getUser().getStreetId());
        PageData<ApplyDTO> page = allowanceApplyService.page(params);
        return new Result<PageData<ApplyDTO>>().ok(page);
    }

如上述实现,通过获取认证用户的街道ID并附带在查询条件中实现在后续的数据过滤操作。

但这样实现会产生一个新的问题,假如项目接口非常多,有非常多的分页 、列表的类似的权限过滤操作,这时使用上述方式会带来接口文件很多重复性的编码(即设置过滤条件 ),而且Mapper文件(这里使用Mybatis举例,也可以是其它框架)也会有很多重复性编码(即使用过滤条件)。

2.方法切面

为解决上述重复设置、使用过滤条件问题,需要用到自定义的注解并实现AOP界面类来实现这些重复性代码操作。

首先创建一个SegmentedRole标记为注解,内容如下:

SegmentedRole.java

java 复制代码
package com.uav.common.annotation;

import java.lang.annotation.*;

/**
 * 需细分权限的方法注解
 * @author seaua
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SegmentedRole {

    String tableAlias() default "";
}

该类中的tableAlias方法即要查询表的简写,即在Mapper文件中使用的查询简写,如:tb_student简写为ts.

然后对该注解进行自定义的实现,创建一个SegmentedAspect类,该类为该注解方法的主要实现类,内容如下:

SegmentedAspect.java

java 复制代码
package com.uav.common.aspect;

import cn.hutool.core.util.ObjectUtil;
import com.uav.common.annotation.SegmentedRole;
import com.uav.common.constant.Constant;
import com.uav.common.enums.SuperAdminEnum;
import com.uav.common.exception.ErrorCode;
import com.uav.common.exception.SysException;
import com.uav.common.interceptor.DataScope;
import com.uav.common.security.SecurityUser;
import com.uav.common.security.UserDetail;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
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 java.lang.reflect.Method;
import java.util.Map;

/**
 * 数据过滤,切面处理类
 * @author seaua
 */
@Aspect
@Component
public class SegmentedAspect {

    @Pointcut("@annotation(com.uav.common.annotation.SegmentedRole)")
    public void dataFilterCut() {

    }

    @Before("dataFilterCut()")
    public void dataFilter(JoinPoint point) {
        Object params = point.getArgs()[0];
        if(params instanceof Map){
            UserDetail user = SecurityUser.getUser();

            //如果是超级管理员,则不进行数据过滤
            if(user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
                return ;
            }

            try {
                //否则进行数据过滤
                //TODO 获取用户所在街道、社区、网格,进行注入
                Map map = (Map)params;
                String sqlFilter = getSqlFilter(user, point);
                map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));
            }catch (Exception e){

            }
            return ;
        }

        throw new SysException(ErrorCode.DATA_SCOPE_PARAMS_ERROR);
    }

    private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
        SegmentedRole dataFilter = method.getAnnotation(SegmentedRole.class);

        //获取表的别名
        String tableAlias = dataFilter.tableAlias();
        if(StringUtils.isNotBlank(tableAlias)){
            tableAlias +=  ".";
        }
        StringBuilder sqlFilter = new StringBuilder();
        sqlFilter.append(" (");
        if (ObjectUtil.isNotNull(user.getStreetId())){
            sqlFilter.append(tableAlias).append(Constant.TB_STREET_ID);
            sqlFilter.append("=").append(user.getStreetId());
        }
        if (ObjectUtil.isNotNull(user.getCommunityId())){
            if(sqlFilter.length() > 1){
                sqlFilter.append(" and ");
            }
            sqlFilter.append(tableAlias).append(Constant.TB_COMMUNITY_ID);
            sqlFilter.append("=").append(user.getCommunityId());
        }
        if (ObjectUtil.isNotNull(user.getGridId())){
            if(sqlFilter.length() > 1){
                sqlFilter.append(" and ");
            }
            sqlFilter.append(tableAlias).append(Constant.TB_GRID_ID);
            sqlFilter.append("=").append(user.getGridId());
        }
        return sqlFilter.append(")").toString();
    }

}

主要的实现为

java 复制代码
 Map map = (Map)params;
 String sqlFilter = getSqlFilter(user, point);
 map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));

这里将用户的所在市区、镇街、街道信息添加到Map中并通过后续的mybatis拦截器进行sql注入操作。

DataFilterInterceptor.java

java 复制代码
package com.uav.common.interceptor;

import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.util.Map;
import java.util.Properties;

/**
 * 数据过滤
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataFilterInterceptor extends AbstractSqlParserHandler implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

        // SQL解析
        this.sqlParser(metaObject);

        // 先判断是不是SELECT操作
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }

        // 针对定义了rowBounds,做为mapper接口方法的参数
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        String originalSql = boundSql.getSql();
        Object paramObj = boundSql.getParameterObject();

        // 判断参数里是否有DataScope对象
        DataScope scope = null;
        if (paramObj instanceof DataScope) {
            scope = (DataScope) paramObj;
        } else if (paramObj instanceof Map) {
            for (Object arg : ((Map) paramObj).values()) {
                if (arg instanceof DataScope) {
                    scope = (DataScope) arg;
                    break;
                }
            }
        }

        // 不用数据过滤
        if(scope == null){
            return invocation.proceed();
        }

        // 拼接新SQL
        originalSql = getSelect(originalSql, scope);

        // 重写SQL
        metaObject.setValue("delegate.boundSql.sql", originalSql);
        return invocation.proceed();
    }

    private String getSelect(String originalSql, DataScope scope){
        try {
            Select select = (Select) CCJSqlParserUtil.parse(originalSql);
            PlainSelect plainSelect = (PlainSelect) select.getSelectBody();

            Expression expression = plainSelect.getWhere();
            if(expression == null){
                plainSelect.setWhere(new StringValue(scope.getSqlFilter()));
            }else{
                AndExpression andExpression =  new AndExpression(expression, new StringValue(scope.getSqlFilter()));
                plainSelect.setWhere(andExpression);
            }

            return select.toString().replaceAll("'", "");
        }catch (JSQLParserException e){
            return originalSql;
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

过滤器的主要作用是对查询上面添加到查询map中的sql与查询的SQL在查询前进行组装,封装为新SQL。

这样在代码中的设置条件就可以转换为在分页、列表接口上添加SegmentedRole注解,使用方法如下:

java 复制代码
    @GetMapping("page")
    @SegmentedRole(tableAlias = "taa")
    @ApiOperation("分页查询")
    @RequiresPermissions("sys:apply:page")
    public Result<PageData<ApplyDTO>> page(@ApiIgnore @RequestParam Map<String, Object> params) {
        //TODO  对应区域权限只能查看区域内的数据
        PageData<ApplyDTO> page = allowanceApplyService.page(params);
        return new Result<PageData<ApplyDTO>>().ok(page);
    }

这样在后续的数据库查询中就会自动拼接SQL以实现接口的数据过滤了。


如果你对区块链 内容感兴趣可以查看我的专栏:小试牛刀-区块链

感谢您的关注和收藏!!!!!!

相关推荐
洛_尘4 分钟前
Java EE进阶2:前端 HTML+CSS+JavaScript
java·前端·java-ee
转转技术团队1 小时前
转转上门隐私号系统的演进
java·后端
皮皮林5511 小时前
Java+Selenium+快代理实现高效爬虫
java
hqxstudying2 小时前
Java行为型模式---策略模式
java·开发语言·建造者模式·适配器模式·策略模式
lxsy2 小时前
spring-ai-alibaba 简化版NL2SQL
java·nl2sql·spring-ai·ai-alibaba
WanderInk2 小时前
依赖对齐不再“失联”:破解 feign/BaseBuilder 错误实战
java·后端·架构
菜鸡上道2 小时前
Maven入门指南:生命周期、阶段和执行顺序详解
java·maven
许苑向上2 小时前
分布式缓存击穿以及本地击穿解决方案
java·分布式·缓存
爱Java&Java爱我2 小时前
数组:从键盘上输入10个数,合法值为1、2或3,不是这三个数则为非法数字,试编辑统计每个整数和非法数字的个数
java·开发语言·算法
共享家95272 小时前
linux-线程互斥
java·开发语言·jvm