✨重磅!盹猫的个人小站正式上线啦~诚邀各位技术大佬前来探秘!✨
这里有:
- 硬核技术干货:编程技巧、开发经验、踩坑指南,带你解锁技术新姿势!
- 趣味开发日常:代码背后的脑洞故事、工具测评,让技术圈不再枯燥~
- 独家资源分享:开源项目、学习资料包,助你打怪升级快人一步!
👉 点击直达→ 盹猫猫的个人小站 👈
🌟 来逛逛吧,说不定能挖到你正在找的技术宝藏哦~
目录
欢迎来到 盹猫(>^ω^<)的博客
本篇文章主要介绍了
**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以实现接口的数据过滤了。
如果你对区块链 内容感兴趣可以查看我的专栏:小试牛刀-区块链
感谢您的关注和收藏!!!!!!
