基于SpringAOP实现数据权限控制

Posted 大扑棱蛾子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于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()

以上是关于基于SpringAOP实现数据权限控制的主要内容,如果未能解决你的问题,请参考以下文章

springAOP实现基于注解的数据源动态切换

shiro中基于注解实现的权限认证过程

提高数据的安全性和可控性,数栈基于 Ranger 实现的 Spark SQL 权限控制实践之路

如何使用 Asp.Net Core 实现基于权限的访问控制

基于Vue实现后台系统权限控制

杨老师课堂之springAOP事务控制源码解析