基于SpringAOP实现数据权限控制

基于SpringAOP实现数据权限控制

在此主要是实现对用户查询数据返回字段的控制。比如一个表格有A,B,C,D,E五列,用户U1只能查看A,B,C三列。

此文章讲述的内容并不能实现在查询时仅查询A,B,C三列,而是在查询后做过滤,将D,E两列的值置为空。

本文只启到抛砖引玉的作用,代码并没有完全实现。只写了核心部分。如果大家用到的话,还需要根据自己项目的权限体系完善。

准备工作

首先定义注解QueryMethod,用于标注方法是查询方法。

/**
 * 标识此方法为查询方法,可能会受到数据权限控制,理论上所有查询方法都应该加上此注释
 *
 * @author Wang Chengwei
 * @since 1.0.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface QueryMethod {
}

定义查询方法返回的结果

/**
 * 支持过滤的结构,用于在AOP方法中对要显示的数据进行控制
 *
 * @author Wang Chengwei
 * @since 1.0.0
 */
public class FilterableResult<T> implements Filterable<T>, MetaSetter {

    @Getter
    @Setter
    private List<T> rows;
    private List<SysDataResource> meta;

    @Override
    public void doFilter(Function<T, T> filterFunc) {
        for (T row : rows) {
            filterFunc.apply(row);
        }
    }

    @Override
    public void setMeta(List<SysDataResource> dataResources) {
        this.meta = dataResources;
    }

    @Override
    public List<SysDataResource> getMeta() {
        return this.meta;
    }

    public static <T> FilterableResult<T> build(List<T> rows) {
        FilterableResult<T> result = new FilterableResult<>();
        result.setRows(rows);
        return result;
    }
}

/**
 * 支持过滤
 *
 * @author Wang Chengwei
 * @since 1.0.0
 */
public interface Filterable<T> {

    /**
     * 遍历列表,执行过滤方法
     * @param filterFunc 过滤方法
     */
    void doFilter(Function<T, T> filterFunc);
}

/**
 * 设置数据结构
 *
 * @author Wang Chengwei
 * @since 1.0.0
 */
public interface MetaSetter {

    /**
     * 设置数据结构,用于前台展示
     * @param dataResources 数据结构
     */
    void setMeta(List<SysDataResource> dataResources);

    /**
     * 获取数据结构
     * @return 数据结构
     */
    List<SysDataResource> getMeta();
}

SysDataResource为数据资源项。


@Table(name = "tbsd_sys_data_resource")
public class SysDataResource {
    /**
     * 数据ID
     */
    @Id
    @Column(name = "data_id")
    private String dataId;

    /**
     * 权限ID
     */
    @Column(name = "authority_id")
    private String authorityId;

    /**
     * 数据项名称
     */
    @Column(name = "data_name")
    private String dataName;

    /**
     * 数据标志符号
     */
    @Column(name = "data_code")
    private String dataCode;

    /**
     * 创建时间
     */
    @Column(name = "create_time")
    private Date createTime;

    // 扩展字段

    /**
     * 是否允许访问
     */
    @Column(name = "is_accessible")
    private Boolean isAccessible;
}

用户数据资源权限说明

系统权限对应数据资源,权限中设置访问数据的业务方法。

authorityName: 用户查询
authorityMark: AUTH_USER_QUERY
classAndMethod: com.wawscm.shangde.module.security.service.impl.SysUserServiceImpl.findUser(int,int)

classAndMethod要明确到实现类,本文档中的方法不支持接口方法。

用户拥有此权限后就可以设置对应的数据资源访问权限。

资源名称标识
用户IDuserId
用户名username
密码password
用户姓名name

用户的资源权限设置如下

资源名称标识isAccessible
用户IDuserIdtrue
用户名usernametrue
密码passwordfalse
用户姓名namefalse

SysUser bean代码如下

@Table(name = "tbsd_sys_user")
public class SysUser {
    /**
     * 用户ID
     */
    @Id
    @Column(name = "user_id")
    private String userId;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 用户姓名
     */
    private String name;

    /**
     * 手机号
     */
    @Column(name = "phone_num")
    private String phoneNum;

    /**
     * 用户状态(1-正常;2-冻结)
     */
    @Column(name = "user_state")
    private String userState;

    /**
     * 用户类型(1-系统管理员;2-分店管理员;3-便利店管理员;4-普通用户)
     */
    @Column(name = "user_type")
    private String userType;

    /**
     * 店铺ID(总部用户字段为空)
     */
    @Column(name = "store_id")
    private String storeId;

    /**
     * 最后一次登陆时间
     */
    @Column(name = "last_login_time")
    private Date lastLoginTime;

    /**
     * 创建时间
     */
    @Column(name = "create_time")
    private Date createTime;
}

拦截方法过滤数据

主要根据SysDataResource.isAccessible来判断是否有字段的访问权限,如果值为false则认为没有权限,其他字段不管,因为数据的权限控制,可能只是控制某几个字段,而不是全部。比如一些id类的字段。我们不希望在设置数据资源时还要设置表格中并不显示的字段。

核心代码如下。

/*
 * Copyright © 2016-2018 WAWSCM Inc. All rights reserved.
 */
package com.wawscm.shangde.interceptor;

import com.wawscm.shangde.base.Filterable;
import com.wawscm.shangde.base.MetaSetter;
import com.wawscm.shangde.base.SystemSettings;
import com.wawscm.shangde.module.security.helper.UserAuthorityHelper;
import com.wawscm.shangde.module.security.model.SysDataResource;
import com.wawscm.shangde.utils.ShiroUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.beans.PropertyDescriptor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 数据权限拦截器
 *
 * @author Wang Chengwei
 * @since 1.0.0
 */
@Component
@Aspect
public class DataResourceAuthorityInterceptor {

    @Autowired
    private UserAuthorityHelper userAuthorityHelper;

    @Autowired
    private SystemSettings systemSettings;

    /**
     * 切入点设置,拦截所有具有{@link com.wawscm.shangde.annotation.QueryMethod}注解的方法
     */
    @Pointcut("@annotation(com.wawscm.shangde.annotation.QueryMethod)")
    public void queryMethodPointcut() {
    }

    /**
     * 环绕通知
     * @param joinPoint ProceedingJoinPoint
     * @return 方法返回的对象
     * @throws Throwable 方法执行时抛出的异常,此处不做任何处理,直接抛出
     */
    @Around(value = "queryMethodPointcut()")
    public Object doInterceptor(ProceedingJoinPoint joinPoint) throws Throwable {
        Object object = joinPoint.proceed();
        String methodName = this.getMethodName(joinPoint);
        if (object != null) {
            if (object instanceof Filterable) {
                this.doFilter((Filterable) object, methodName);
            }

            if (object instanceof MetaSetter) {
                this.metaHandler((MetaSetter)object, methodName);
            }
        }
        return object;
    }

    /**
     * 执行过滤操作
     * @param filterable 方法返回的对象
     * @param methodName 拦截的方法名称
     */
    private void doFilter(Filterable<?> filterable, String methodName) {
        List<SysDataResource> resources = this.getDataResources(methodName);

        // 如果
        if (CollectionUtils.isEmpty(resources)) {
            return;
        }

        filterable.doFilter(o -> {
            Map<String, SysDataResource> dataColumnMap = new HashMap<>(resources.size());
            for (SysDataResource column : resources) {
                dataColumnMap.put(column.getDataCode(), column);
            }

            PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(o.getClass());
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                String name = propertyDescriptor.getName();
                SysDataResource dataColumn = dataColumnMap.get(name);
                if (dataColumn != null && !dataColumn.getIsAccessible()) {
                    try {
                        propertyDescriptor.getWriteMethod().invoke(o, new Object[] {null});
                    } catch (Exception ex) {
                        // skip
                    }
                }
            }
            return o;
        });
    }

    /**
     * 设置数据结构
     * @param metaSetter 方法返回的对象
     * @param methodName 拦截的方法名称
     */
    private void metaHandler(MetaSetter metaSetter, String methodName) {
        List<SysDataResource> resources = this.getDataResources(methodName);
        if (resources != null) {
            metaSetter.setMeta(resources);
        } else { // 如果没有设置数据资源,默认用户拥有访问全部资源的权限
            List<SysDataResource> allResources = findAuthorityDataResource(methodName);
            metaSetter.setMeta(allResources);
        }
    }

    /**
     * 根据方法名和用户ID获取用户的数据权限
     * @param methodName 拦截的方法名称
     * @return 用户的数据权限
     */
    private List<SysDataResource> getDataResources(String methodName) {
        String userId = ShiroUtil.getUserId();
        return this.userAuthorityHelper.getDataResource(methodName, userId);
    }

    /**
     * 获取此方法对应的所有数据资源项
     * @param methodName 拦截的方法名称
     * @return 用户的数据权限
     */
    private List<SysDataResource> findAuthorityDataResource(String methodName) {
        return null; // 此处代码省略
    }

    private String getMethodName(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        // systemSettings.isSupportMethodParams()表示是否支持方法参数,默认支持。如果设置为不支持,则权限中的方法应设置为com.wawscm.shangde.module.security.service.impl.SysUserServiceImpl.findUser
        if (systemSettings.isSupportMethodParams() && signature instanceof MethodSignature) {
            MethodSignature methodSignature = (MethodSignature)signature;

            StringBuilder sb = new StringBuilder();

            sb.append(methodSignature.getDeclaringTypeName());
            sb.append(".");
            sb.append(methodSignature.getName());
            sb.append("(");
            Class<?>[] parametersTypes = methodSignature.getParameterTypes();
            for (int i = 0; i < parametersTypes.length; i++) {
                if (i > 0) {
                    sb.append(",");
                }
                Class<?> parametersType = parametersTypes[i];
                sb.append(parametersType.getSimpleName());
            }
            sb.append(")");
            return sb.toString();
        } else {
            StringBuilder sb = new StringBuilder();
            sb.append(signature.getDeclaringTypeName());
            sb.append(".");
            sb.append(signature.getName());
            return sb.toString();
        }
    }
}

UserAuthorityHelper代码如下,此处的数据均为模拟数据。正确的做法应该是从数据库或缓存中获取

/**
 * 用户权限工具类
 *
 * @author Wang Chengwei
 * @since 1.0.0
 */
@Component
public class UserAuthorityHelper {

    public List<SysDataResource> getDataResource(String methodName, String userId) {
        List<SysDataResource> resources = new ArrayList<>();
        SysDataResource resource1 = new SysDataResource();
        resource1.setDataCode("userId");
        resource1.setDataName("用户ID");
        resource1.setIsAccessible(true);

        SysDataResource resource2 = new SysDataResource();
        resource2.setDataCode("username");
        resource2.setDataName("用户名");
        resource2.setIsAccessible(true);

        SysDataResource resource3 = new SysDataResource();
        resource3.setDataCode("password");
        resource3.setDataName("密码");
        resource3.setIsAccessible(false);

        SysDataResource resource4 = new SysDataResource();
        resource4.setDataCode("name");
        resource4.setDataName("用户姓名");
        resource4.setIsAccessible(false);

        resources.add(resource1);
        resources.add(resource2);
        resources.add(resource3);
        resources.add(resource4);

        return resources;
    }
}

SysUserServiceImpl代码如下,此处的数据也是模拟数据

/**
 * 用户业务
 *
 * @author Wang Chengwei
 * @since 1.0.0
 */
@Service
public class SysUserServiceImpl implements SysUserService {


    @Override
    @QueryMethod
    public FilterableResult<SysUser> findUser(int page,  int pageNum) {
        List<SysUser> users = new ArrayList<>();
        users.add(mockUser());
        users.add(mockUser());
        users.add(mockUser());
        users.add(mockUser());
        users.add(mockUser());
        users.add(mockUser());
        users.add(mockUser());
        users.add(mockUser());
        users.add(mockUser());
        users.add(mockUser());

        System.out.println("返回的数据");
        System.out.println(JsonKit.toJson(users));
        return FilterableResult.build(users);
    }

    private SysUser mockUser() {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(UUIDGenerator.genertate());
        sysUser.setUsername(UUIDGenerator.genertate());
        sysUser.setName(UUIDGenerator.genertate());
        sysUser.setPassword(UUIDGenerator.genertate());
        sysUser.setPhoneNum(UUIDGenerator.genertate());
        sysUser.setCreateTime(new Date());
        sysUser.setLastLoginTime(new Date());

        return sysUser;
    }
}

测试

public class SysUserServiceImplTest extends BaseSpringTestCase {

    @Autowired
    private SysUserService sysUserService;

    @Test
    public void findUser() {
        FilterableResult<SysUser> users = this.sysUserService.findUser(1, 15);
        System.out.println("过滤后的数据");
        System.out.println(JsonKit.toJson(users));
    }
}

过滤前的数据为

{
    "rows": [
        {
            "userId": "838563855e3e44489d6dc91c8a37031a",
	        "username": "b6f89f7ec27e434e92638a063b310a66",
	        "password": "0ec85df1f31f4d88b9efbb62c46863f9",
	        "name": "3cf146b6f13c46ef9372c19f734fa712",
	        "phoneNum": "e42d86e8212943a7926515cc5aaf0dab",
	        "lastLoginTime": "2018-01-05 18:47:52",
	        "createTime": "2018-01-05 18:47:52"
        },
        {
            "userId": "8cfcd7ccaa3442edb8c4175e5e4e7e9e",
	        "username": "632f1491d576486bb936d7da8ddf1bf6",
	        "password": "acc506932c194adf963de57a3f651ac6",
	        "name": "dfa65420b26f4222abc3e4477ec0efc4",
	        "phoneNum": "619e24618a894368b3d3f4a242bc9a81",
	        "lastLoginTime": "2018-01-05 18:47:52",
	        "createTime": "2018-01-05 18:47:52"
        }
        ......
    ]
}

过滤后的数据为

{
    "rows": [
        {
            "userId": "838563855e3e44489d6dc91c8a37031a",
            "username": "b6f89f7ec27e434e92638a063b310a66",
            "phoneNum": "e42d86e8212943a7926515cc5aaf0dab",
            "lastLoginTime": "2018-01-05 18:47:52",
            "createTime": "2018-01-05 18:47:52"
        },
        {
            "userId": "8cfcd7ccaa3442edb8c4175e5e4e7e9e",
            "username": "632f1491d576486bb936d7da8ddf1bf6",
            "phoneNum": "619e24618a894368b3d3f4a242bc9a81",
            "lastLoginTime": "2018-01-05 18:47:52",
            "createTime": "2018-01-05 18:47:52"
        }
        ......
    ],
    "meta": [
        {
            "dataName": "用户ID",
            "dataCode": "userId",
            "isAccessible": true
        },
        {
            "dataName": "用户名",
            "dataCode": "username",
            "isAccessible": true
        },
        {
            "dataName": "密码",
            "dataCode": "password",
            "isAccessible": false
        },
        {
            "dataName": "用户姓名",
            "dataCode": "name",
            "isAccessible": false
        }
    ]
}

从结果上可以看出password,name这两个字段已经被过滤掉了,同时增加了meta数据结构内容。前台可以根据meta中返回的数据来创建表格,实现表格的动态显示。


原创文章,转载请注明出处!

  • 3
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
项目描述 说明: spring security 全注解式的权限管理 动态配置权限,角色和资源,权限控制到按钮粒度 采用token进行权限校验,禁用session,未登录返回401,权限不足返回403 采用redis存储token及权限信息 内置功能: 用户管理:用户查询、添加用户、修改用户、给用户分配角色 菜单管理:菜单列表、添加菜单、修改菜单、删除菜单、权限配置、菜单图标设置、菜单排序 角色管理:角色查询、添加角色、修改角色、删除角色 代码生成:根据表名生成bean、controller、dao、Mapper.xml、列表页、搜索、分页、新增页、修改页 job集群:创建job、取消job、查询job、下拉搜索spring bean 数据源监控:druid 接口swagger文档 日志查询 邮件管理:发送邮件、搜索邮件 文件管理:上传文件、文件列表、文件删除 公告管理:公告未读提醒、发布公告、查询公告、公告阅读人列表 excel下载:自定义sql导出excel、也可在页面展示sql结果数据 字典管理:一些常量字典的维护 个人信息修改 修改密码 头像修改 其他说明: 日志模块 sl4j日志分包:将sql日志、业务日志、异常日志进行了分离,更方便定位问题 日志表:使用aop拦截实现 权限控制:基于token方式,禁用session 对各种不同异常进行了全局统一处理 使用lombok简化java代码,让源码更简洁,可读性高 mybatis未进行二次封装,原滋原味,简单sql采用注解,复杂sql采用Mapper.xml配置 使用了layui的弹出层、菜单、文件上传、富文本编辑、日历、选项卡、数据表格等 表单数据采用bootstrapValidator校验,简单快捷方便 运行环境 jdk8+mysql+redis+IntelliJ IDEA+maven 项目技术(必填) Springboot+Mybatis+ SpringMvc+springsecrity+Redis+bootstrap+jquery 数据库文件 压缩包内 jar包文件 maven搭建
基于SpringBoot+SpringCloud的微服务大型在线学习平台实现【服务端源码+数据库】.zip 项目介绍 学成在线就是一个在线学习平台,类似慕课网以及腾讯学堂,是专门针对IT行业进行在线学习的平台。 学成在线采用B2B2C的业务模式,即向企业与个人提供平台实现教学服务,其中企业就是老师,提供课程,作业,考试等;个人就是学生,通过平台实现教学和学习的过程。 其原型就是腾讯课堂。 重点了解微服务技术栈: 学成在线服务端基于Spring Boot构建,采用Spring Cloud微服务框架。 持久层:MySQL、MongoDB、Redis、ElasticSearch 数据访问层:使用Spring Data JPA 、Mybatis、Spring Data Mongodb等 业务层:Spring IOC、Aop事务控制Spring Task任务调度、Feign、Ribbon、Spring AMQP、Spring Data Redis等 控制层:Spring MVC、FastJSON、RestTemplate、Spring Security Oauth2+JWT等 微服务治理:Eureka、Zuul、Hystrix、Spring Cloud Config等 已经实现的功能 CMS管理:页面增删改查,页面预览,页面发布 课程管理:课程增删改查,课程计划增删改查,课程营销信息,课程图片上传删除,课程发布,课程预览,课程搜索 媒资管理:视频上传,视频处理,媒资增删改查,媒资与课程计划关联 学习中心:视频播放,课程计划查询,动态获取视频地址 认证中心:登录,登出,查询JWT用户信息,权限校验管理,Zuul网关路由,拦截 订单服务:支付成功后自动添加选课(分布式事物)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大扑棱蛾子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值