在此主要是实现对用户查询数据返回字段的控制。比如一个表格有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
中返回的数据来创建表格,实现表格的动态显示。
原创文章,转载请注明出处!