Skip to content

Instantly share code, notes, and snippets.

@Exodia
Last active August 10, 2024 05:20
Show Gist options
  • Save Exodia/e8d0b0d28b3339ed4e79 to your computer and use it in GitHub Desktop.
Save Exodia/e8d0b0d28b3339ed4e79 to your computer and use it in GitHub Desktop.
对通用业务模块权限控制的一些思路

现状

我们现有需要控制权限的地方大概有以下几个点:

  1. 入口控制,这点在配置文件中体现

  2. 数据接口控制, 这点体现在请求数据源以及对数据源做筛选的场景

  3. 视图展现控制,这点体现在视图层上根据权限情况控制视图内容的展现

入口控制

这块和通用模块完全解耦无关联,这里不讨论

数据接口控制和视图展现控制

大部分的数据源请求接口是这样做的:

var XX_LIST = {
   xxList: function (model) {
	if(model.checkPermission(‘canGetXXList’)) {
		return model.data(‘xx’).list();
	}
	return [];
   }
};

上述代码中实现了两个具体的逻辑:有权限时干什么,无权限时又干什么。

问题

数据源接口在有权限时要干什么:这个逻辑我们确实可以认为是通用的,这块依赖 data 层的服务接口,具体的实现是 data 层的事了。

数据源接口在无权限时要干什么: 这个逻辑我们目前认为也是通用的,即返回一个空数组,但实际上并不是这样,无权限要干什么这个需求其实是和各个业务项目之间紧密关联的

再进一步的和视图展现的权限控制结合起来,在视图层面上,通用模块层面也是认为,只要当前用户对某个操作没有权限,那么相应的操作在视图上就隐藏起来。 然而,这是通用模块在需求上的意淫。

举个实际的例子:

mssp 的用户需要有创建信息流代码位的权限才能创建信息流代码位,于是按照目前的实现,我们会这样写:

var SLOT_TYPES = {
	slotTypes: function (model) {
		var types = SLOT_TYPE.toArray(); 
		// 无权限,过滤掉信息流代码位类型
		if(!model.checkPermission(‘canCreateFeed’)) {
			types = u.reject(types, {value: SLOT_TYPE.FEED});
		}
		return types;
	}
}

很自然的将信息流代码位在无权限时过滤掉。

然而在实际的场景中,最开始是信息流过滤不展示的,随着业务发展,为了鼓励用户使用信息流代码位,于是需求变了,变成了:无权限时,也展示信息流代码位,当无权限用户选择信息流代码位时,右侧出现信息流权限申请的提示,引导用户申请信息流权限。

所以有个很合理的业务场景是,有些无权限的操作在视图上不展示,有些无权限的操作我们需要引导用户去开通权限,这种场景在有 vip收费模型的业务中及其常见

有了上面的场景,我们再重新思考下无权限时,过滤相关数据,视图上不展示这个逻辑是否是通用的?

方案

过滤相关数据,视图上不展示这个逻辑 这是一个具体实现,我认为目前是能够满足70%场景的。

有权限时,拉取数据 这也是一个具体实现,基本满足所有场景。

方案1

回到示例的代码中,获取代码位类型这个功能肯定是有的,有权限时获取代码位类型的行为是符合需求的,所有业务线都可以重用,无权限时获取到代码位的行为能够满足70%的场景,其余30%的场景需要能够让业务线定制扩展,所以目前的解决方案一种可以是将可能变化的数据源抽象为一个方法,方法中带有默认实现,30%的场景重写该接口即可:

var SLOT_TYPES = {
	slotTypes: function (model) {
		return model.getSlotTypes();
	}
};
SlotFormModel.prototype.getSlotTypes = function () {
	// 有权限的实现
	var types = SLOT_TYPE.toArray(); 
	// 无权限,过滤掉信息流代码位类型
	if(!model.checkPermission(‘canCreateFeed’)) {
		types = u.reject(types, {value: SLOT_TYPE.FEED});
	}
	return types;
};

// 重写场景, 无论是否有权限,都展示所有的数据
CustomSlotFormModel.prototype.getSlotTypes = function () {
	return SLOT_TYPE.toArray();
};

该方案问题在于,基类的重写,需要重复的编写父类有权限时的实现代码,在很多场景下,有权限时的实现逻辑并不会像实例代码中仅有一行,而且子类也不应该感知父类的具体实现。

方案2

在方案1的问题上,我们继续改进,既然有权限的实现是通用的,无权限的实现是定制的,那么我们就把无权限的功能抽象为一个接口,子类重写即可。

var SLOT_TYPES = {
	slotTypes: function (model) {
		return model.checkPermission(‘canCreateFeed’) ?  model.getSlotTypes() : model.getUnAuthSlotTypes();
	}
};
SlotFormModel.prototype.getSlotTypes = function () {
	// 有权限的实现
	return SLOT_TYPE.toArray(); 
};

SlotFormModel.prototype.getUnAuthSlotTypes = function () {
      // 无权限,过滤掉信息流代码位类型
	return  u.reject(SLOT_TYPE.toArray(), {value: SLOT_TYPE.FEED});
};

// 重写场景, 无论是否有权限,都展示所有的数据
CustomSlotFormModel.prototype.getUnAuthSlotTypes = function () {
	return this.getSlotTypes();
};

方案2解决了方案1的有权限下的逻辑无法复用的问题。

但问题在于,如果需要权限校验的功能多了,同时又要满足业务线对无权限时的特化,意味着每个功能我们做三个工作

  1. 实现有权限时的通用逻辑为一个方法,给业务线复用,这快基本都是通用的,覆盖场景较小。

  2. 实现一套默认的无权限时的通用逻辑,抽象为一个方法,默认实现能够满足的业务线直接复用,不满足的业务线定制无权限时的实现。

  3. 在 datasource 配置中做权限分支逻辑的路由。

再进一步,方案1的问题是有权限的通用代码无法复用,方案2的问题是为了通用代码的复用,将变化的地方抽象为接口。

方案3

换一个思路,一个典型的软件系统包含一些核心的关注点和系统级的关注点,比如通用代码位模块的核心关注点就是代码位的创建/修改功能,而系统级的关注点则是日志、授权、安全、性能等

系统关注点会在多个模块中出现,我们可以将这些系统关注点独立为可复用的模块,比如我们系统现在的 SystemPermission 模块。

接着我们在通用模块中给模块抽象了一层 ModulePermission,在模块中又加入这些权限的逻辑判断,从而导致了我们上述的种种问题。事实上,我们在通用模块中耦合了系统层面的逻辑,虽然是抽象了一层权限接口,但无可避免的限定了系统在有权限时干啥,没权限时又干啥的特定实现

我们既然明白系统在有权限时干的都是一样的事,并且目前的通用模块权限里都是默认权限全开的,那何不在通用模块中直接去掉权限逻辑

var SLOT_TYPES = {
	slotTypes: function (model) {
		return model.getSlotTypes();
	}
};

SlotFormModel.prototype.getSlotTypes = function () {
	// 不考虑权限的通用实现
	return SLOT_TYPE.toArray(); 
};

// 重写场景, 无权限需要过滤的时候
CustomSlotFormModel.prototype.getSlotTypes = function () {
	var types = this.$super(arguments);
	// 业务系统自己实现无权限时的行为
	if(!this.checkPermission(‘canCreateFeed’)) {
		types = u.reject(types, {value: SLOT_TYPE.FEED})
	}
	return types;
};

三个方案对比:

我们将有权限下的逻辑实现成本设为 A,无权限下的逻辑实现成本设为B,增加新接口带来的成本设为C,通用代码增加权限逻辑带来的成本为D,无权限的默认实现场景覆盖率为70%,无权限需要定制的场景覆盖率为30%

  1. 方案1在业务类要定制无权限实现的场景下,必须重复编写通用模块的逻辑,平均成本为: 0.3A + 0.3B + D

  2. 方案2需要增加一个无权限的接口,带有一个默认实现,定制场景重写该方法,平均成本为: 0.3B + C + D

  3. 方案3可以避免通用逻辑的重复,减少通用模块中权限判断的逻辑,不足之处是在无需定制无权限实现的场景下,业务类也必须自己重复实现,平均成本为:0.7B + 0.3B = B

方案1和方案2成本对比是0.3A 和 C 的取舍, 这两者之间我倾向是方案2占优,方案1带来的是一种 repeat youself 的坏味道,且 A 的复杂度一般都是比较高的,而新增一个接口带来的是业务类需要增加一个感知成本,相比30%场景下重复实现 A 来说,成本会更小很多。

方案1,2与方案3的对比是0.3A+D,C+D,0.7B的取舍。

从我个人角度出发,A 的复杂度是远大于 B 的5倍以上,即 0.3A > 0.7B,因此这里方案1首先被排除。

我们再来对比方案2和3,也就是 C+D 和 0.7B 的取舍,前面说过 C 带来的是一个感知成本,D 带来的是一个 Permission 模块 + 通用模块的权限判断逻辑代码,而 B 的默认实现基本都是一行代码解决,即 return []; 0.7B 基本可以被 C 以及model.checkPermission的成本抵消,因此方案2会比方案3多出一个 Permission 模块,而 Permission 模块则又限定了业务类必须将实现封装成 Permission 的接口。

同时还有一个问题:我们目前的权限检测逻辑非常简单,假设权限检测变成异步的,我们的通用模块中的权限校验逻辑则失效了。

以上比较来看,方案3的成本目前看来是最优的,我们下一步是解决方案3的问题,即消除0.7B

未来的方案4 - 基于 AOP 的权限拦截

方案3中,业务类需要自己做 model.checkPermission检测,同时70%的业务类还是需要去编写通用的无权限实现,这部分的逻辑是通用的,我们抽出来作为一个通用服务类:CommonPermission

CommonPermission.prototype.checkPermission =  function (authString) {
	// 具体实现
};

CommonPermission.prototype.authList =  function (instance, method, auth) {
	if(!this.checkPermission(auth)) {
		// 默认实现
		return [];
	}
	// 有权限则调用实例的方法
	return instance[method];
};

// 无权限创建信息流时,代码位的通用实现
SlotPermission.prototype.authSlotTypes = function(instance, method) {
	if(!this.canCreateFeedSlot()) {
		// 默认实现
		return u.reject(SlotTypes.toArray(), {value: SlotTypes.FEED});
	}
	// 有权限则调用实例的方法
	return instance[method];
};

// config.js
ioc.addComponment(‘commonPermission’, {module: ‘common/Permission’});
ioc.addComponment(‘slotPermission’, {module: ‘ssp-slot/Permission’});
ioc.aop.addAspect({
	$ref: ‘slotPermission’,
	pointCut:’slotModel.getSlotTypes’,
	avdice: {
		before: {
			method: ‘authSlotTypes’,
			args: ‘SSP_VIEW_SLOT’
		}
	}
});

以上,我们将无权限的默认实现抽出为一个服务,在 aop 配置中声明我们要在 slotModel.getSlotTypes方法执行前,先调用slotPermission#authSlotTypes进行权限验证,如果验证不通过则返回筛选过的数组,否则返回调用slotModel.getSlotTypes方法的结果。

在有需要定制化无权限实现的场景下,继承SlotPermission ,将aop配置的$ref指向为你的子类 Permission 的方法即可。对于 mssp 来说,无论有权限还是无权限场景都是返回全数据,因此完全不需要进行 aop 配置。

方案4与方案3相比,消除了0.7B成本,消除了通用代码的权限判断逻辑,新引入了SlotPermission 提供了默认无权限的实现逻辑满足了70%的场景,同时又提供了扩展性。

更多的切面:日志,异常捕获,性能测试

在有了 aop 之后,像日志,异常捕获,性能测试都可以无缝织入模块中,而开发模块的开发者只开发模块的核心功能:

Log.prototype.log = funciton (instance, method) {
 	console.log(‘call method:,  method);
	return instance[method];
};

Performance.prototype.start = function (instance, method) {
	this.startTime = Date.now();
	this.methodName = method;
	return instance[method];
};

Performance.prototype.end = function () {
	this.endTime = Date.now();
	this.result = this.endTime - this.startTime;
	console.log(this.methodName,  time elapsed:, this.result)
};

Exception.prototype.exec = function(instance, method, e) {
	console.log(‘method error:, e);
};

// 给 getSlotTypes 添加日志功能
ioc.aop.addAspect({
	$ref: ‘log’,
	pointCut:’slotModel.getSlotTypes’,
	avdice: {
		before: {
			// getSlotTypes开始前调用 Log#log 方法打印日志
			method: ‘log’
		}
	}
});

// 给 getSlotTypes 添加性能测试
ioc.aop.addAspect({
	$ref: ‘performance’,
	pointCut:’slotModel.getSlotTypes’,
	avdice: {
		before: {
			// getSlotTypes开始前调用 Performance#start 
			method: ‘start’
		},
		after: {
			// getSlotTypes执行结束后调用 Performance#start 
			method: ‘end’
		}
	}
});

// 给 getSlotTypes 添加异常打印
ioc.aop.addAspect({
	$ref: ‘exception’,
	pointCut:’slotModel.getSlotTypes’,
	avdice: {
		exception: {
			// getSlotTypes 异常时调用  Exception#exec
			method: ‘exec’
		}
	}
});

以上是很粗糙的代码,详细的参数设计需要等开始搞 aop 的时候再进行。

总结

在 aop 的模型下,上述的系统关注点很容易的可以抽象为公共的工具服务,通过配置的方式切入到各个商业逻辑通用模块中,而不再需要在商业逻辑代码中进行这些系统层面的操作。

带来的好处是:1. 通用化了工具类, 2. 通用业务模块代码变得更加简洁。

但目前来看,我们的aop框架还未诞生,好在我们无权限时的逻辑不是很复杂,可以考虑方案3作为过渡方案。

在 aop 模型下,我们的模块结构变成如下: aop

@otakustay
Copy link

通常来说,不大会倾向把AOP的方案用在业务相关的逻辑上,而如日志、性能等业务无关的时候则常用

如果在无权限时的逻辑是通用的,则这不是一个业务相关的逻辑。反之分权限控制不同的逻辑,则是一个业务相关的东西

我在你的基础上设想了一个方案,我们可以用一次Wrapper的模型来做这事,这是利用类AOP的思想,但不是将它套到AOP的实现上去

首先我们去定义一个接口,在JavaScript中接口肯定是逻辑上的,不会有实际的代码

SlotFormModel {
    {Array} getSlotTypes();
}

首先模块在实现这个接口的时候可以不管权限

SlotFormModel.prototype.getSlotTypes = function () {
    // 有权限的实现
    return SLOT_TYPE.toArray(); 
};

然后我们的业务中的实现是关心权限的,但它并不继承模块的实现

我们的目标是在方案1的基础上来实现模块实现的逻辑的复用,因此我们只要能拿到模块的实现就行了

CustomSlotFormModel.prototype.setCoreModel = function (coreModel) {
    this.coreModel = coreModel;
};

CustomSlotFormModel.prototype.getSlotTypes = function () {
    var types = this.coreModel.getSlotTypes();
    if (!this.checkPermission('canCreateFeed')) {
        types = u.reject(types, ...);
    }
    return types;
};

当然我们可以拿AOP做这事,但是aspect中参杂实际影响业务的逻辑可能会是一件不那么舒服的事,导致业务的逻辑相对分散在非核心的代码文件中

@Exodia
Copy link
Author

Exodia commented Aug 31, 2015

不大会倾向把AOP的方案用在业务相关的逻辑上

aop这块我暂时还没有比较实际的应用经验,权限这块也主要是参考后端的解决方案,后端在权限和认证这块倒是经常用 aop 去做。

当然,我们和后端的区别在于,一个业务模块有自有的一个权限类,而不是像后端那样权限专门集中在一个类中去做,我觉得后端并不将权限放在每个 mvc 模块中的主要考虑是:

  1. 权限是跨越 mvc 三层的逻辑,并认为权限不是 mvc 需要去关心的,而是由系统层面去管理。
  2. 不在 mvc 中引入权限逻辑判断,独立权限出来,通过 aop 的方式切入。这样能够随意变更权限接口,而不需要在 mvc 中去更改这些权限逻辑代码。这样也使得mvc 模块的扩展复用性变得更高。

这块我再调研调研。

wrapper 的解决方案问题

假设我们在 通用模块不引入权限逻辑 的前提下(这个前提需要大家一起讨论后形成定论),

如果按照这种方案去搞,我倒是觉得通用模块不加入权限判断的逻辑,子模块直接继承通用模块似乎就行了。

SlotFormModel.prototype.getSlotTypes = function () {
   // 直接实现有权限时候的逻辑
};

CustomModel.protoype.getSlotTypes = function () {
   if (this.checkPermission('canCreateFeed')) {
          reutrn this.$super(arguments);
    }
   // 无权限时的逻辑
};

由此引出的另一个问题

如果通用模块不引入权限逻辑,我们的系统的权限模块似乎可以集中在一个类中去做,而 mvc 下也不用这些 Permission 了,毕竟我们的权限逻辑还是很简单的,一个类是可以搞定的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment