公司已有的代理商管理系统,权限控制需要调整,我负责前端相关工作,此文记录一下修改过程。
前端原有的权限控制是基于角色的,即根据当前登录账号的角色,控制菜单及功能入口的显示/隐藏状态。前端主要用到框架Angular和Angular-ui-router,在这前提下,原有的权限控制实现方式大致如下:
在
mainController
中获取当前登录用户的角色,并将其赋值到$rootScope.role
,页面中根据该变量做基于角色的权限控制。具体实现即使用ng-show="role === ?"
控制元素显示/隐藏,达到不同角色显示不同内容的效果
上述实现的最大问题在于前端权限控制严重依赖系统固有角色,极度僵硬,角色变动会给前端带来巨大冲击。原有的丑陋实现之所以能够存在,跟技术人员的水平有一定关系,同时也因为系统初期相对简单,只包含3个角色(root、agent、user),才没有暴露出问题。随系统功能增加,角色与权限都需要再细分,按照之前的实现方式,很难维护,不得已才重新设计与实现前端的权限控制。
同时由于历史原因,存在这样一种情况:
本是相同性质的接口,却按角色不同,拆分成了不同接口。比如日志查询,root调用的是
/logs/findAllLogs
,agent调用的是/logs/findAgentLogs
这是系统设计时犯的一个错误,分权分域,域和权的混淆。为处理上述不同角色调用不同接口的情况,在其他controller
中也经常也需要使用变量$rootScope.role
,因此在这些controller
中依赖角色信息的代码,需要等待mainController
中角色信息获取成功后方可执行。
要解决前端权限控制僵硬的问题,核心就是权限控制不应依赖角色,而是依赖登录账号所拥有的权限项,这需要后台做两个修改:
后台配合修改后,接下来考虑前端实现,实现方式完全可以参照原有实现,只需要将角色概念替换成权限项。在mainController
中获取账号权限项,赋值给$rootScope.permissions
,提供是否拥有权限的判断方法$rootScope.permissionExists = function (item) {...};
。剩下的都是体力活,删除页面上跟role
相关的逻辑,再按照新的方式(ng-show="permissionExists(?)"
)为页面上的元素绑定权限。
虽然参照原有方式实现没啥明显缺陷,但自己对于Angular不熟悉,基于探索和学习的精神,打算使用自定义指令与服务达到相同效果,同时了解如何实现Angular自定义指令与服务。
使用指令,最终设想的效果与ng-show="permissionExists(permission)"
类似,提供一个指令xsAccess
,页面使用xs-access="pId"
的方式为UI元素绑定所需权限,其中pId
表示具体权限项标识符,xs则是命名空间约束,避免冲突。
由于Angular正常注册指令只能在初始化阶段同步注册,为达到上述效果,所面临的一个问题:
由于权限项是通过Ajax异步获取的,在注册指令时,账号拥有的权限项并不确定,Angular页面渲染也先于Ajax返回,因此,在获取权限项的过程中,相关UI元素的的显示/隐藏状态也就无法确定
为解决上述问题,采用如下方式(代码参见文章底部):
在数据获取过程中,将指令的link方法缓存,数据获取完成时,再调用一次link方法。
上述方式虽能保持异步获取权限项成功后,保证页面上权限相关元素状态正确,但更好的方式可能是使用运行时动态注册指令,下次有机会再实践。
使用指令的方式绑定权限已经实现,但还需考虑随登陆状态变化权限项也要重新获取,主要原因如下:
基于Angular-ui-router实现的单页面应用,在用户不刷新页面的情况下,不同
state
间跳转,应用不会重新初始化。在第一次初始化时,xsAccess内部获取并持有用户权限项信息,而当用户在不同state
间跳转,权限项可能发生变化,如logout => login
、login => logout
。当这种情况发生,xsAccess内部维护的权限项信息不再有效,此时界面跟权限相关元素的状态也可能错误
为解决上述问题,考虑了两种方案:
loginCtrl
、logoutCtrl
)触发该事件,达到更新目的第一种方案,指令竟然对外提供事件触发机制去保持状态正确,直觉上很怪异,用过的一些指令也没见这样搞事的,所以选择第二种看起来相对科学一点的方式。注册一个xsAccess的服务,对外提供fresh
方法用于刷新。同时需要考虑权限获取过程中,相关UI元素的的显示/隐藏状态无法确定,在权限获取完成时及时更新这些UI元素的状态。
最终,也就是注册一个指令用于界面权限绑定,以及注册一个服务维护当前账号权限项信息。由于项目已使用RequireJs做模块化,指令和服务直接通过局部变量共享信息,省略部分细节,主要代码如下:
// 项目使用RequireJs做模块化,app即应用module
// 事后认为单独注册一个module,然后在service上暴露两个方法(init、fresh)更合理
define([], function () {
var permissions = [];
var fetching = true;
var dirties = [];
function init(app) {
app.directive('xsAccess', function () {
return {
restrict: 'A',
scope: null,
link: function (scope, element, attrs) {
if (fetching) {
var that = this;
dirties.push(function () {
that.link.call(that, scope, element, attrs);
});
element.hide();
return;
}
_elementStatus(element, attrs);
}
};
function _elementStatus(element, attrs) {
// todo 根据元素所需权限显示/隐藏
}
}).factory('xsAccess', ['$http', function ($http) {
fetchPermissions();
return {
fresh: fetchPermissions
};
function fetchPermissions() {
fetching = true;
var url = '/svc/userPermission';
$http.get(url).success(function (res) {
permissions = res;
fetching = false;
dirties.forEach(function () {
item.call(null);
});
dirties = [];
}).error(function (err) {
// todo error handling
});
}
}]);
}
return {
init: init
};
});
博客原文