简介
Apache Open For Business(Apache OFBiz) 是Apache开源的一个经典ERP项目。它提供了一套企业应用,用于集成以及自动化一些企业的“商业流程”。
从学习角度来看,它也是一个非常不错的企业级应用框架。这篇文章从OFBiz的权限设计这一切入点来谈谈OFBiz对于应用系统的权限设计。
设计思想简述
OFBiz采用的“安全组”(Security Group)来将"权限"跟"用户"联系起来。系统中有若干种权限,比如系统预置的权限、用户自定义的权限、资源的权限、操作的权限等等,这些权限会跟安全组建立关系(多对多的关系),而用户又与安全组建立关系(也是多对多的关系)。
其中,系统预置权限,是以XML配置的方式导入数据表的。这些配置文件通常的路径为{Component/Application baseDir}/data/XXXSecurityData.xml。这里有对整个权限设计相关表的初始化数据。
权限控制级别
OFBiz对于权限有如下几个控制级别:
登录级别
在每个Component的根目录下的ofbiz-component.xml文件下,有对于访问该component的“最基本的权限”定义。所谓最基本的权限,就是登录该component的用户需要至少拥有该文件内定义的权限才可以访问。示例:
见其中的“base-permission”属性。可以看到它包含了两个权限值——OFBTOOLS/FACILITY,这也意味着你必须同时拥有这两个权限才能访问该组件。而通常一个Component也会同时包含权限“OFBTOOLS”以及权限“COMPONNENT-NAME_VIEW”,这样配置的目的是OFBTOOLS用于对web app的访问进行控制,而COMPONNENT-NAME_VIEW用于控制浏览web app的信息。
component 菜单级别
component的顶级菜单显示的组件将只对登录过的用户(并且这些用户至少具有“WEBAPP-NAME_VIEW”或者“COMPONENT_NAME-ADMIN”权限)显示,这跟登录级别的限制相似。这种级别的访问控制实现在“appbar.ftl”中用以控制显示哪些应用程序的tab bar。
request(controller.xml)级别
这里有两个重要的参数,在每个component的webapp下的controller.xml中的每个request(<request-map)标签有一个security(<security)标签,包含了两个属性:
- https:定义是否对该请求应用SSL加密
- auth:定义是否需要登录才能执行该请求,因此只有在登录成功以及在其他级别上的安全检查通过后,该请求才会被执行
<!-- Request Mappings --> <request-map uri="MarketingReport"> <security https="true" auth="true"/> <response name="success" type="view" value="MarketingReport"/> </request-map>
screen级别
- permission:标识位于哪个component
- action:标识执行的动作
Freemarker模板片段级别
service定义级别
service编程级别
- Minilanguage:使用<check-permission 标签,注:Minilanguage是OFBiz特有的基于XML的“语言”。
- Java:使用org.ofbiz.security.Security.API
记录级别
角色受限的(或者基于角色)权限(又称Party Roles)
安全角色
数据表结构设计
首先来看看OFBiz对于权限这块的表结构设计,这里一共牵扯到6个数据表:
- SECURITY_GROUP
- SECURITY_PERMISSION
- SECURITY_GROUP_PERMISSION
- USER_LOGIN_SECURITY_GROUP
- PARTY_RELATIONSHIP
- SECURITY_PERMISSION_AUTO_GRANT(不作讨论)
SECURITY_GROUP
SECURITY_PERMISSION
- PERMISSION_ID:权限名称:通常以形如“Application_Operate”的形式进行定义(其中,Application表示具体的应用名称,Operate表示操作名称,常用的有CREATE/UPDATE/...)。当然了,也有一些特殊的命名方式比如:“MARKETING_VIEW”表示对MARKETING应用的页面拥有查看权限;"MANUAL_PAYMENT"表示人工支付的事务操作权限;"MARKETING_ADMIN"这里ADMIN作为后缀是一个特殊,表示它具有对MARKETING应用的所有操作权限。
- DESCRIPTION:对于PERMISSION_ID的简短描述
SECURITY_GROUP_PERMISSION
USER_LOGIN_SECURITY_GROUP
PARTY_RELATIONSHIP
关键代码解读
看完了数据表设计,下面我们来看看代码实现,权限相关的代码被封装为"security"component,位于{Base_dir}/framework/security/src文件夹下,主要操作被抽象在名为Security的接口中,接口主要包含几个关键方法:
public Iterator<GenericValue> findUserLoginSecurityGroupByUserLoginId(String userLoginId); public boolean securityGroupPermissionExists(String groupId, String permission); public boolean hasPermission(String permission, HttpSession session); public boolean hasEntityPermission(String entity, String action, HttpSession session); public boolean hasRolePermission(String application, String action, String primaryKey, String role, HttpSession session); public void clearUserData(GenericValue userLogin);
其中,
第一个方法是根据用户Id查询用户权限组;
第二个方法是check某个安全组是否拥有某个权限
第三个方法是判断某用户是否拥有某个权限(这里用户的相关信息被封装在session中)
第四个方法判断用户是否拥有某个实体的操作权限
第五个方法判断是否拥有某角色的权限
第六个方法清楚跟用户相关的所有缓存数据(该方法由framework在用户退出登录的时候调用)
注:上面三个hasXXX 方法都有不同的重载
再来看看OFBiz中的默认实现(OFBizSecurity.java)中的关键代码:
public boolean hasEntityPermission(String entity, String action, GenericValue userLogin) { if (userLogin == null) return false; // if (Debug.infoOn()) Debug.logInfo("hasEntityPermission: entity=" + entity + ", action=" + action, module); Iterator<GenericValue> iterator = findUserLoginSecurityGroupByUserLoginId(userLogin.getString("userLoginId")); GenericValue userLoginSecurityGroup = null; while (iterator.hasNext()) { userLoginSecurityGroup = iterator.next(); // if (Debug.infoOn()) Debug.logInfo("hasEntityPermission: userLoginSecurityGroup=" + userLoginSecurityGroup.toString(), module); // always try _ADMIN first so that it will cache first, keeping the cache smaller if (securityGroupPermissionExists(userLoginSecurityGroup.getString("groupId"), entity + "_ADMIN")) return true; if (securityGroupPermissionExists(userLoginSecurityGroup.getString("groupId"), entity + action)) return true; } return false; }
这是对于hasEntityPermission的最终实现,我们可以看到,它会首先尝试在entity中追加“ADMIN”字符串,也就是说,先查看超级权限,如果拥有超级权限,则直接认为拥有权限,否则才会去查看细粒度的具体权限。
public boolean hasRolePermission(String application, String action, String primaryKey, List<String> roles, GenericValue userLogin) { String entityName = null; EntityCondition condition = null; if (userLogin == null) return false; // quick test for special cases where were just want to check the permission (find screens) if (primaryKey.equals("") && roles == null) { if (hasEntityPermission(application, action, userLogin)) return true; if (hasEntityPermission(application + "_ROLE", action, userLogin)) return true; } Map<String, String> simpleRoleMap = OFBizSecurity.simpleRoleEntity.get(application); if (simpleRoleMap != null && roles != null) { entityName = simpleRoleMap.get("name"); String pkey = simpleRoleMap.get("pkey"); if (pkey != null) { List<EntityExpr> expressions = new ArrayList<EntityExpr>(); for (String role: roles) { expressions.add(EntityCondition.makeCondition("roleTypeId", EntityOperator.EQUALS, role)); } EntityConditionList<EntityExpr> exprList = EntityCondition.makeCondition(expressions, EntityOperator.OR); EntityExpr keyExpr = EntityCondition.makeCondition(pkey, primaryKey); EntityExpr partyExpr = EntityCondition.makeCondition("partyId", userLogin.getString("partyId")); condition = EntityCondition.makeCondition(exprList, keyExpr, partyExpr); } } return hasRolePermission(application, action, entityName, condition, userLogin); }
上面的代码可以看到,在方法内部会先尝试调用hasEntityPermission,如果没有权限,则尝试在application后面追加"_ROLE"字符串来查看角色权限是否拥有,如果拥有则,直接返回,否则才会根据相关的Entity-NameRole表继续查找。
Security_Group VS RBAC
我曾经在参与的一个项目中,接触过常用的RBAC(role based access control)权限设计思想。也曾转载过一篇OA的权限设计文章。在OFBiz中弱化了角色的概念,而强化了“安全组”的概念。我认为是因为系统规模的不同造成的设计差异。RBAC常见于单一的系统设计,在单一的系统中,角色这个词定位准确而清晰;而在OFBiz中,它的目标是构建出一套ERP的平台(包含多个异构系统)。在跨越多个系统之上谈角色,反而变得模糊不清,导致混乱,但采用安全组的概念却不至于,安全组的概念使得权限的载体的粒度更细、更灵活,但同时也更为繁杂,因此其实OFBiz中还是有角色这个概念的(体现在权限中包含_ROLE的权限,可将其视为角色权限)。而常用的RBAC中通常也用到这种“安全组”的概念(一个特殊用户,需要分配有跨越多个角色的权限时,这时需要对该用户的权限进行定制化,这是就用得上类似的安全组的概念)。
因此我认为它们各适合不同的场景,但是可以互相结合的。