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

举报
盹猫 发表于 2025/07/19 15:46:08 2025/07/19
【摘要】 本文介绍了在SpringBoot+Shiro框架中实现多级权限数据过滤的解决方案。针对传统权限注解只能控制接口访问的问题,提出了两种实现方式:1. 代码内设置条件:在分页接口中手动添加区域过滤条件;2. 方法切面:通过自定义@SegmentedRole注解和AOP切面自动注入SQL过滤条件。重点讲解了第二种方案,包括创建注解、实现切面类以及MyBatis拦截器自动拼接SQL的具体实现。该方法通过注

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


这里有:

 


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


👉 点击直达→ 盹猫猫的个人小站 👈
🌟 来逛逛吧,说不定能挖到你正在找的技术宝藏哦~

目录

一、简介

二、分析

1.代码内设置条件

2.方法切面


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


本篇文章主要介绍了

[Shiro实现多级权限的分页查询]
❤博主广交技术好友,喜欢文章的可以关注一下❤

一、简介

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

二、分析

1.代码内设置条件

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

    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

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

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();
    }

}

主要的实现为

 Map map = (Map)params;
 String sqlFilter = getSqlFilter(user, point);
 map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));

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

 DataFilterInterceptor.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 注解,使用方法如下:

    @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以实现接口的数据过滤了。


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

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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。