要实现此功能有很多方法,如果系统中多个地方都需要类似的需求,那我们就可以将其提出来做成一个通用的功能。这里我介绍一个相对简单的解决方案,以供参考。

一、 整体架构





微服务实现数据权限控制_微服务


image


​数据权限​​为作一个​​注解​​的形式挂在每一个需要数据权限控制的Controller上,由于和具体的程序逻辑有关故有一定的入侵性,且需要数据库配合使用。

二、 实现流程






image


  1. 浏览器传​​带查询权限范围参数​​访问Controller,如​​cities​


POST http://127.0.0.1:8000/order/query
accept: */*
Content-Type: application/json
token: 1e2b2298-8274-4599-a26f-a799167cc82f

{"cities":["cq","cd","bj"],"userName":"string"}


  1. 通过注解拦截权限范围参数,并根据预授权范围比较,回写在授权范围内的权限范围参数

    cities = ["cq","cd"]
  2. 通过参数传递到DAO层,在SQL语句中拼装出查询条件,实现数据的过滤

    select * from order where city in ('cq','cd')

三、 实现步骤

1. 注解实现

注解的完整代码,请详见​​源代码​

1)创建注解


@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface ScopeAuth {

String token() default "AUTH_TOKEN";
String scope() default "";
String[] scopes() default {};
}


此注解为运行时​​RetentionPolicy.RUNTIME​​作用在方法上​​ElementType.METHOD​​的

​token​​:获取识别唯一用户的标识,与用户数据权限存储有关

​scope​​,​​scopes​​:预请求的数据权限范围

2) AOP实现注解


public class ScopeAuthAdvice {

@Around("@annotation(scopeAuth)")
public Object before(ProceedingJoinPoint thisJoinPoint, ScopeAuth scopeAuth) throws Throwable {
// ... 省略过程
// 获取token
String authToken = getToken(args, scopeAuth.token(), methodSignature.getMethod());
// 回写范围参数
setScope(scopeAuth.scope(), methodSignature, args, authToken);

return thisJoinPoint.proceed();
}

/**
* 设置范围
*/
private void setScope(String scope, MethodSignature methodSignature, Object[] args, String authToken) {
// 获取请求范围
Set<String> requestScope = getRequestScope(args, scope, methodSignature.getMethod());
ScopeAuthAdapter adapter = new ScopeAuthAdapter(supplier);
// 已授权范围
Set<String> authorizedScope = adapter.identifyPermissionScope(authToken, requestScope);
// 回写新范围
setRequestScope(args, scope, authorizedScope, methodSignature.getMethod());
}

/**
* 回写请求范围
*/
private void setRequestScope(Object[] args, String scopeName, Collection<String> scopeValues, Method method) {
// 解析 SPEL 表达式
if (scopeName.indexOf(SPEL_FLAG) == 0) {
ParseSPEL.setMethodValue(scopeName, scopeValues, method, args);
}
}
}


此为演示代码省略了过程,主要功能为通过token拿到预先授权的数据范围,再与本次请求的范围做交集,最后回写回原参数。

过程中用到了较多的​​SPEL表达式​​,用于计算表达式结果,具体请参考​​ParseSPEL文件​

3)权限范围交集计算


public class ScopeAuthAdapter {

private final AuthQuerySupplier supplier;

public ScopeAuthAdapter(AuthQuerySupplier supplier) {
this.supplier = supplier;
}

/**
* 验证权限范围
* @param token
* @param requestScope
* @return
*/
public Set<String> identifyPermissionScope(String token, Set<String> requestScope) {
Set<String> authorizeScope = supplier.queryScope(token);

String ALL_SCOPE = "AUTH_ALL";
String USER_ALL = "USER_ALL";

if (authorizeScope == null) {
return null;
}

if (authorizeScope.contains(ALL_SCOPE)) {
// 如果是全开放则返回请求范围
return requestScope;
}

if (requestScope == null) {
return null;
}

if (requestScope.contains(USER_ALL)){
// 所有授权的范围
return authorizeScope;
}

// 移除不同的元素
requestScope.retainAll(authorizeScope);

return requestScope;
}
}


此处为了方便设置,有两个关键字范围

  • ​AUTH_ALL​​:预设所有范围,全开放的意思,为数据库预先设置值,请求传什么值都通过
  • ​USER_ALL​​:请求所有授权的范围,请求时传此值则会以数据库预设值为准

4) spring.factories自动导入类配置


org.springframework.boot.autoconfigure.AutoConfigurationImportSelector=\
fun.barryhome.cloud.annotation.ScopeAuthAdvice


如果注解功能是单独项目存在,在使用时有可能会存在找不到引入文件的问题,可通过此配置文件自动载入需要初始化的类

2. 注解使用


@ScopeAuth(scopes = {"#orderDTO.cities"}, token = "#request.getHeader(\"X-User-Name\")")
@PostMapping(value = "/query")
public String query(@RequestBody OrderDTO orderDTO, HttpServletRequest request) {
return Arrays.toString(orderDTO.getCities());
}


在需要使用数据权限的controller方法上增加​​@ScopeAuth​​注解

​scopes = {"#orderDTO.cities"}​​:表示取输入参数orderDTOcities值,这里是表达式必须加#

实际开发过程中,需要将orderDTO.getCities()带入后续逻辑中,在DAO层将此拼装在SQL中,以实现数据过滤功能

3. 实现AuthStoreSupplier

​AuthStoreSupplier​​接口为数据权限的存储接口,与AuthQuerySupplier配合使用,可按实际情况实现

此接口为非必要接口,可由数据库或Redis存储(推荐),一般在登录的同时保存在​​Redis​​中

4. 实现AuthQuerySupplier

​AuthQuerySupplier​​接口为数据权限查询接口,可按存储方法进行查询,推荐使用Redis


@Component
public class RedisAuthQuerySupplier implements AuthQuerySupplier {

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 查询范围
*/
@Override
public Set<String> queryScope(String key) {
String AUTH_USER_KEY = "auth:logic:user:%s";
String redisKey = String.format(AUTH_USER_KEY, key);

List<String> range = redisTemplate.opsForList().range(redisKey, 0, -1);

if (range != null) {
return new HashSet<>(range);
} else {
return null;
}
}
}


在分布式结构里,也可将此实现提出到权限模块,采用远程调用方式,进一步解耦

5. 开启数据权限


@EnableScopeAuth
@EnableDiscoveryClient
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}


四、 综述

至此数据权限功能就实现了。在微服务器架构中为了实现功能的复用,将注解的创建AuthQuerySupplier的实现提取到公共模块中,那么在具体的使用模块就简单得多了。只需增加​​@ScopeAuth​​注解,配置好查询方法就可以使用。

五、源代码

文中代码由于篇幅原因有一定省略并不是完整逻辑,如有兴趣请Fork源代码

​https://gitee.com/hypier/barry-cloud/tree/master/cloud-auth-logic​



作者:barry的异想世界

链接:https://www.jianshu.com/p/ea3540202e43

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。