背景
企业应用系统逐渐增多后,各系统单独管理各自的用户数据容易形成信息孤岛,分散的用户管理模式阻碍了企业应用向平台化演进,为降低企业系统设计开发、集成成本,打通技术闭环完善系统设计实现流程、优化提升系统设计实现的整体体验,各业务系统高扩展、易集成、易维护性显得尤为重要,通过高效、统一、标准化、低代码、易操作、可视化托拉拽的自助式权限控制配置功能轻松实现各业务系统间集成。
术语定义及说明
RBAC:Role-Base Access Control,基于角色的访问控制。
权限谓词:用于描述或判定权限性质、特征或权限之间关系的表达式。
概述
主要特征
主要特点可以概括为非侵入、易配置、易维护、松耦合、细粒度、易扩展等等。
- 非侵入式,通过统一的权限校验服务和过滤器进行资源鉴权。
- 易配置,通过配置文件配置通俗易懂的权限表达式。
- 易维护,新增服务组件或资源权限项可通过更新权限表达式配置文件内容轻松实现鉴权管理。
- 松耦合,与业务服务解耦可轻松方便实现外部系统功能集成。
- 细粒度,权限表达式支持数据权限控制,轻松实现权限细粒度控制。
- 易扩展,各业务系统间易集成,无需修改太多源代码。
权限架构
主体采用RBAC(Role-Base Access Control,基于角色的访问控制)模型,就是用户通过角色和权限进行关联实现,多对多的用户角色关系模式。 主要由用户管理、角色管理、菜单管理几部分组成,角色权限管理包括功能菜单权限、操作权限,数据权限控制,权限主要由功能权限(界面权限、菜单权限、操作权限)和数据权限构成。
技术架构
系统整体采用微服务架构,安全模块采用OAuth2开放式授权标准,Token令牌采用JWT标准实现,技术框架采用SpringCloud+SpringGateway+SpringSecurity+自定义权限表达式。 OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
权限配置
密钥配置
系统遵循OAuth2开放式授权标准协议,Token令牌采用RSA非对称加密算法加密,系统用户密码采用MD5加密。
权限表达式配置
鉴权(authentication)是指验证用户是否拥有访问系统的权利,鉴权配置即配置哪些资源需要什么权限才能访问,配置资源与权限之间的关系。
权限配置文件位于resources/security.yml
在介绍权限表达式配置之前需要先了解下权限表达式、URL路径、权限谓词3个概念, 权限表达式描述资源权限的逻辑关系,包括请求方式、URL路径、权限谓词3个部分, 请求方式即HTTP请求方法,如:GET、POST、PUT、DELETE等,URL路径即需要鉴权的URL路径地址,支持多种通配符匹配,权限谓词用于描述或判定权限性质、特征或权限之间关系的表达式。
格式语法
目前支持两种类型的权限表达语法,分别功能权限表达式、数据权限表达式。
功能权限表达式
HTTP请求方法 URL路径=权限谓词表达式 红色部分:HTTP请求方法,例如GET、POST、PUT、DELETE等
黄色部分:空格占位符
绿色部分:URL表达式,支持多种通配符。例如:/app/*/users,app/**
橙色部分:等于号占位符,描述左侧匹配资源需要右侧权限。
蓝色部分:权限谓词,例如:(view&&export)||edit表示需要查看与导出权限,或者编辑权限。
数据权限表达式
HTTP请求方法 URL路径参数=权限谓词表达式参数 浅绿色部分:参数占位符,数据定位参数顾名思义用于唯一标识行级数据的参数,支持多参数标识,支持批量行数据定位鉴权。
URL路径表达式
URL路径表达式,用于配置需要进行鉴权的资源路径地址。 路径有三种通配符匹配方法,这些可以组合出很多种灵活的路径模式
通配符 | 描述 |
---|---|
? | 匹配任何单字符 |
* | 匹配0或者任意数量的字符 |
** | 匹配0或者更多的目录 |
示例: |
Path | 描述 |
---|---|
/app/*.x | 匹配所有在app路径下的.x文件 |
/app/p?ttern | 匹配/app/pattern 和 /app/pXttern,但是不包括/app/pttern |
/**/example | 匹配/app/example, /app/foo/example, 和 /example |
/app/**/dir/file | 匹配 /app/dir/file.jsp/app/foo/dir/file.html/app/foo/bar/dir/file.pdf/app/dir/file.java |
/ */ .jsp | 匹配(Matches)任何的.jsp 文件 |
数据权限表达式URL通配符增加了参数占位符控制 |
通配符 | 描述 |
---|---|
? | 匹配任何单字符 |
* | 匹配0或者任意数量的字符 |
** | 匹配0或者更多的目录 |
{参数名} | 匹配参数,单条数据,例如:参数值1 |
[参数名] | 匹配参数,批量数据,例如:参数值1,2,3,100 |
示例:
Path | 描述 |
---|---|
/app/*.x | 匹配所有在app路径下的.x文件 |
/app/p?ttern | 匹配/app/pattern 和 /app/pXttern,但是不包括/app/pttern |
/**/example | 匹配/app/example, /app/foo/example, 和 /example |
/app/**/dir/file | 匹配/app/dir/file.jsp/app/foo/dir/file.html/app/foo/bar/dir/file.pdf/app/dir/file.java |
/ */ .jsp | 匹配任何.jsp 文件 |
/app/{id}/app/[id] | 匹配所有在app路径下的路径,相当于/app/*,{id}、[id]为参数占位符 |
/app/{type}/[id] | 匹配app路径下两级目录,相当于/app/ / , {type}、[id]为参数占位符app/家具/1app/电脑/100 |
/*/example{type}[id] | 相当于/*/example,{type}、[id]为参数占位符匹配/app/example, /app/foo/example, 和 /example |
权限谓词
支持与或非逻辑运算: 例如: a&&b, a||b, (a&&b)||(c&&d), a&&b||c, !a, !(a&&b),例如:view&&export)||edit表示校验是否拥有查看和导出的权限,或者编辑权限。
参数
数据权限控制需要唯一标识行数据,用于验证用户是否有该数据的操作权限,参数占位符为数据定位器的参数名,支持单参数、复合参数,单条数据定位鉴权({参数名}大括号),批量数据定位鉴权([参数名]中括号)。例如单参数userId用户编号,productId产品编号,复合参数type类型、productId产品编码,批量数据权限鉴权userIds用户编码集,需要指出的是每个表达式运算的数据对象是每一条数据。
示例:
参数类型 | 数据类型 | 描述 |
---|---|---|
路径参数 | 单条 | /user/{id}=view |
/user/{id}/*=edit | ||
/user/{id}/**=view&&edit | ||
批量 | /user/[id]=view | |
/user/[id]/*=edit | ||
/user/[id]/**=view&&edit | ||
/products/{type}/[id]=view{type}&&download[id] | ||
实体参数 | 单条 | /user{id}=view |
/user{id}=edit | ||
/user{id}=view&&edit | ||
/products{type}{id}=view{type}&&download{id} | ||
批量 | /user[id]=view | |
/user[id]=edit | ||
/user[id]=view&&edit | ||
/products{type}[id]=view{type}&&edit[id] | ||
/productstype=view[type]&&edit[id] | ||
实体层级参数 | 单条 | /*{t1:t2:id}=edit,表示鉴权参数为{"t1":{"t2":{"id":"2","info":"info"}}} |
批量 | /*[t1:t2:ids]=edit,表示鉴权参数为{"t1":{"t2":{"ids":"1,2,3","info":"info"}}} |
一个参数时权限项表达式参数默认可以不配置/user/{id}=view&&eidt等于/user/{id}=view{id}&&eidt{id} 路径参数
鉴权配置 | GET /user/{id}=view |
---|---|
匹配路径 | GET /user/* |
请求用例 | GET /user/1 |
描述 | 验证当前用户是否有查看获取用户id为1数据的权限 |
鉴权配置 | GET /sales/{productId}/[ids]=view{productId}&&export[ids] |
---|---|
匹配路 | GET/sales/ / |
请求用 | GET/sales/1001/1,2,3 |
描述 | 验证当前用户是否有查看产品类型为1001数据的权限并且有导出订单编码id为1,2,3的三条数据权限(逗号会进行转义) |
实体参数
鉴权配置 | GET /user{id}=view |
---|---|
匹配路径 | GET /user |
请求用例 | GET /user |
请求参数 | {“id”:”1”...} |
描述 | 验证当前用户是否有查看获取用户id为1数据的权限 |
鉴权配置 | GET /user*{id}=view |
---|---|
匹配路径 | GET /user* |
请求用例 | GET /user?id=1 |
描述 | 验证当前用户是否有查看获取用户id为1数据的权限 |
鉴权配置 | GET /user[id]=view |
---|---|
匹配路径 | GET /user |
请求用例 | GET /user?id=1,2,3 |
描述 | 验证当前用户是否有查看和删除用户id为1,2,3三条数据的权限(逗号会进行转义) |
鉴权配置 | GET /user[id]=view |
---|---|
匹配路径 | GET /user |
请求用例 | GET /user |
请求参数 | {“id”:“1,2,3”...} |
[{“id”:“1”...},{“id”:“2”...},{“id”:“3”...}] | |
描述 | 验证当前用户是否有查看和删除用户id为1,2,3三条数据的权 |
复合参数
鉴权配置 | GET /sales{productId}{saleId}=view{productId}&&(export{saleId}||edit{saleId}) |
---|---|
匹配路径 | GET /sales |
请求用例 | GET /sales |
请求参数 | {“productId”:“1001”,“saleId”:“1”...} |
描述 | 验证当前用户是否有查看产品类型为1001数据的权限并且有导出订单或者编辑订单编码id为1的数据权限 |
鉴权配置 | GET /sales{productId}[saleId]=view{productId}&&(export[saleId]||edit[saleId]) |
---|---|
匹配路径 | GET /sales |
请求用例 | GET /sales |
请求参数 | {“productId”:“1001”,“saleId”:“1,2,3”...} |
[{“productId”:”1001”,“saleId”:“1”...},{“productId”:”1001”,“saleId”:“2”...},{“productId”:”1001”,“saleId”:“3”...}] | |
描述 | 验证当前用户是否有查看产品类型为1001数据的权限并且有导出订单或者编辑订单编码id为1、2、3三条数据的权限 |
鉴权配置 | GET /sales{productId}[t1:saleId]=view{productId}&&(export[t1:saleId]||edit[t1:saleId]) |
---|---|
匹配路径 | GET /sales |
请求用例 | GET /sales |
请求参数 | {“productId”:“1001”,“t1”:{“saleId”:“1,2,3”...}} |
{“productId”:”1001”,“t1”:[{“saleId”:“1”...},{“saleId”:“2”...},{“saleId”:“3”...}]} | |
描述 | 验证当前用户是否有查看产品类型为1001数据的权限并且有导出订单或者编辑订单编码id为1、2、3三条数据的权限 |
鉴权配置 | GET /salesproductId= |
---|---|
View[productId]&&(export[saleId] | |
匹配路径 | GET /sales |
请求参数 | [{“productId”:”1001”,“saleId”:“1”...}, |
{“productId”:”1001”,“saleId”:“2”...}, | |
{“productId”:”1001”,“saleId”:“3”...}, | |
{“productId”:”1002”,“saleId”:“4”...}, | |
{“productId”:”1002”,“saleId”:“5”...}] | |
描述 | 验证当前用户是否有查看产品类型为1001数据的权限并且有导出订单或者编辑订单编码id为1、2、3三条数据的权限 |
验证当前用户是否有查看产品类型为1002数据的权限并且有导出订单或者编辑订单编码id为4、5数据的权限 |
元素配置
权限配置由permit-paths白名单、jwt-users来宾用户、resource资源权限3部分元素组成,其中resource元素包括authrities、data-authrities两部分元素。
permit-paths白名单
无需进行权限过滤的请求路径,当系统有些资源不需要进行权限过滤的时候配置该元素,配置语法如下图所示,路径表达式配置。
Resource资源权限
资源权限配置元素为权限模块的核心元素,包括authrities功能权限、data-authrities数据权限,两者的区别在于功能权限为URL匹配路径的权限控制,数据权限在URL匹配路径的基础上增加了数据鉴权控制。
Authrities功能权限
功能权限控制,通常表示界面业务功能接口权限的控制,例如:用户列表数据获取接口,用户信息添加、删除、更新等等接口的权限控制。
data-authrities数据权限
数据权限控制,为更细粒度的权限控制,是对业务行级数据的操作权限进行控制,例如用户只能查看、操作自己创建的数据,管理员可以赋权给指定的用户或角色查看、操作某些某条数据。
微前端
微前端架构是一种类似于微服务的可插拔式架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一单体应用变为多个小型可独立运行、独立开发、独立部署的应用。
前端页面通过微前端加载器,利用页面路由和动态加载等技术,实现前端集成主页面与微前端的“拼图式”开发。前端集成项目团队只需关注前端整体风格、微前端之间的数据交互和页面路由等内容,不涉及前端与后端之间以及后端与后端之间的 API 集成,从而降低集成过程中的技术敏感度、团队沟通成本和集成复杂度,提高交付效率和用户体验。
外部系统前端应用部署集成,各系统前端应用组件独立打包部署,通过配置部署、动态加载应用、统一生命周期管理。
总结
基于权限表达式实现的系统可轻松集成被集成,业务系统作为子系统存在,安全权限管理由宿主统统一处理,将复杂且耦合性高的权限控制从业务服务中剥离,业务子系统只需关注业务逻辑处理,真正做到无缝集成,降低了系统集成的难度与复杂性,提升了系统的整体性能和可维护性。
系统权限模块只是实现了基本的权限控制需求,还存在许多需要完善的地方,例如前端页面功能按钮的权限控制、大数据量的权限数据维护繁琐,需要一条条添加维护好菜单、功能权限、操作权限数据,手动配置鉴权数据极易出错等等。
或许将来有需要可以优化的地方。
- 自动识别功能权限数据
集成新的业务服务维护功能权限数据需要对业务服务接口全面了解,需要一条条维护好功能接口,URL地址、权限标识符极易存在拼写错误,或许可以通过一定的场景自动扫描获取注册服务所有公开的资源接口,管理员可以在系统里通过界面自动查看、搜索资源接口动态可选择地导入接口数据(功能权限数据)。
- 自动初始化操作权限
一般功能操作为增、删、改、查,可以在添加菜单数据后自动初始化操作功能,用户也可以单独添加修改操作功能数据。
- 基础数据权限可配置
根据需要可以通过界面,选择需要进行数据权限控制的业务模块,自动导入数据权限控制基础数据。
- 自助式鉴权配置
用户可以通过可视化界面简单操作配置白名单、来宾用户权限、资源权限等数据,支持鉴权配置数据导入导出等,例如:自动列出所有功能接口通过搜索、勾选等方式选择需要鉴权的资源,引导式配置权限谓词表达式等。