基于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 rows;
    private List meta;

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

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

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

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

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

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

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

    /**
     * 获取数据结构
     * @return 数据结构
     */
    List 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要明确到实现类,本文档中的方法不支持接口方法。

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

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

用户的资源权限设置如下

资源名称 标识 isAccessible
用户ID userId true
用户名 username true
密码 password false
用户姓名 name false

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 resources = this.getDataResources(methodName);

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

        filterable.doFilter(o -> {
            Map 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 resources = this.getDataResources(methodName);
        if (resources != null) {
            metaSetter.setMeta(resources);
        } else { // 如果没有设置数据资源,默认用户拥有访问全部资源的权限
            List allResources = findAuthorityDataResource(methodName);
            metaSetter.setMeta(allResources);
        }
    }

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

    /**
     * 获取此方法对应的所有数据资源项
     * @param methodName 拦截的方法名称
     * @return 用户的数据权限
     */
    private List 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 getDataResource(String methodName, String userId) {
        List 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 findUser(int page,  int pageNum) {
        List 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 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中返回的数据来创建表格,实现表格的动态显示。


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

你可能感兴趣的:(Java开发,Spring专题)