背景:权限是基于系统设定的安全规则或安全策略。用户可以访问并且只能访问他们被授权访问的资源。在实际生产系统中,用户数量非常多,权限的划分需要结合具体的业务场景。一旦强度失控,工作量就会很大。如何解决这个问题呢?例如,对于一个文件系统权限,用户UserA和B只有查看和复制该文件系统下某些文件的权限,而用户C和D不仅有查看和复制文件的权限,还有修改和删除文件的权限。这些权限的划分和授权需要事先获得批准。专门的管理员操作。业界专门提出了一套权限模型和方法,RBAC(Role-BasedAccessControl),是一种基于角色的访问控制方法。其核心概念如下:一个角色(Role)关联一个权限(Permission),一个角色对应多个权限,一个用户(User)关联一个角色(Role),一个用户对应多个角色,以及一个权限(Permission)包含资源,或者结合操作的组合,我们可以让用户通过成为相应角色的成员来获取这些角色的权限,最终达到权限控制的目的。(权限模型图)结合上面的文件系统例子,我们可以用RBAC来描述和描述:文件系统中的文件是权限概念中的“资源”,而删除文件是“操作”,那么我们可以定义一个“文件删除”权限来了。访问文件系统的用户A、B、C、D即为上述模型中的用户。当然,如果用户多的话,我们可以划分“用户群”的概念。对于角色,我们可以分为两种角色,第一种是“文件普通用户”角色,包括“文件查看”和“文件复制”两种权限;二是“文件管理员用户”角色,包含“文件修改”和“文件删除”两种权限。概括地说,基于角色的访问控制方法的访问逻辑表达式是“WhoperformsHowonWhat(Which)”,其逻辑结构从内到外是authority->role->user,即A角色绑定了多个权限,一个用户绑定了多个角色。从图中可以看出,用户A和B都被赋予了“文件普通用户”的角色,即拥有“文件查看”和“文件复制”的权限;用户C和D也被赋予了“文件普通用户”和“文件管理员用户”两个角色,即拥有“文件查看”、“文件复制”、“文件修改”和“文件删除”。如果后面我们觉得“文件复制”存在文件泄露的安全问题,那么我们只需要将其从“文件普通用户”的角色中去掉即可。自然以上四个用户就不能进行文件复制的操作了,所以RBAC模型对于权限的扩缩容非常方便,在讲解完权限系统的基本概念之后,我们来谈谈权限系统在互联网时代的分布式系统,尤其是微服务架构系统中面临的挑战有哪些?它要解决,最适合采用什么框架和技术来解决这些问题?组成麦丹霞业务线体系。我们知道,微服务的优点是可以将业务逻辑清晰的划分出来,让每个微服务承担单一职责的功能。毕竟越是简单的东西,越是稳定。但是,微服务也带来了很多问题。为了完成一个业务操作,我们需要调用很多微服务。那么如何利用权限系统来控制用户对不同微服务的调用,对我们来说是一个挑战。用户系统数量多样、复杂目前采购商夏业务线接入的用户系统数量多样、复杂,数据分散,如公司员工系统(LDAP系统)、公司业务员系统、公司外包人事系统、外网用户系统(客户端使用APP)。不同类型的用户系统可能会访问某些微服务,那么如何使用权限系统来控制不合理的用户对同一个微服务的调用是我们面临的另一个挑战。微服务具有高吞吐量和高可用性要求。业务微服务调用访问权限系统时,不能拖累其吞吐量。当权限系统出现问题的时候,他们的业务调用进程是无法被阻断的,当然业务逻辑也无法改变。现有业务系统快速访问权限新的业务微服务快速访问权限系统相对容易控制,那么对于公司现有的微服务,如何在不改变其架构的情况下快速访问?这也是一个很大的挑战。技术方案及系统架构经过对行业框架的不断选择和比较,我们原本想使用SpringSecurity框架作为我们的权威框架。但经过研究,它有很多优点,但也有两个明显的缺点:框架笨重,权限数据持久层级不好,最重要的是不能做数据库持久化。所以我们决定自己开发。技术方案有几个特点:权限系统的微服务化既然微服务那么多,我们也把权限系统变成一个微服务,用微服务来控制其他微服务的权限,保证整体系统架构的一致性。微服务的统一性和独立性未来Payer的所有业务微服务都会接入权限微服务进行统一管控。权限微服务是一个独立的公共服务。作为众多微服务的一员,必须走麦丹下微服务架构的路线,即业务微服务的权限验证,要经过AlibabaCloudSLB->ZuulAPIGateway,如果是基于web的授权验证,NgnixREST请求代理也需要包括在内。如下图所示:(权限服务与其他公共服务的关系图)业务微服务的代码微入侵我们会使用自定义的权限注解(Annotation),尽可能在业务代码层面增加新的代码,减少负担业务线的负担。高可用,分布式权限缓存基础权限/角色数据我们使用MySQL数据,权限校验数据通过Redis集群缓存。也就是说,当Redis集群中缓存了足够多的权限验证数据后,即使权限微服务全部崩溃也无所谓;相反,当Redis集群崩溃时,只要权限微服务正常运行,权限校验不会受到影响,只是性能会稍微差一点。.支持多种权限,多种调用方式我们会支持业务服务通过RPC进行权限校验,支持其他系统(如Web)通过REST进行权限校验。对于业务服务,主要支持接口注解进行权限拦截验证,即API权限;对于其他系统,一般主要体现在对界面元素的权限校验(比如网页中按钮的Enabled/Disabled,通过权限系统控制),即界面权限。友好的多维度权限/角色/用户入口和绑定接口权限数据导入导出结合SpringOAuth单点登录、SpringSession等,实现安全系统领域的权限扩展。下面详细阐述一下权限微服务的核心技术方案。基于接口的权限控制比较简单,这里略过,主要讲API的权限实现。API权限定义、入库和拦截对于API权限,我们实现了基于注解的扫描入库和拦截,不需要业务服务在web界面输入权限。权限定义API权限使用各个接口或实现类中的方法作为权限资源,每个权限关联一个微服务名称(ServiceName)。我们通过在业务服务的API中添加注解来定义权限。基础设施部门会提供一个权限组件(PermissionComponent)Jar给业务服务部门,里面包含自定义注解。这种实现方式对业务服务影响很小。添加权限机制只是在代码层面添加一些注解。.具体用法如下。对于一个普通的接口类,可以这样定义:@Group(name="UserPermissionGroup",label="UserPermissionGroup",description="UserPermissionGroup")publicinterfaceUserService{@Permission(name="AddUser",label="添加用户")booleanaddUser(@UserIdStringuserId,@UserTypeStringuserType,Useruser);}对于通过Swagger暴露的API,可以这样定义:@Path("/user")@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)@Api(value="Userresourceoperations")@Group(name="UserPermissionGroup",label="用户权限组",description="用户权限组")publicinterfaceUserService{@POST@Path("/addUser/{userId}/{userType}")@Permission(name="AddUser",label="AddUser")booleanaddUser(@PathParam("userId")@UserIdStringuserId,@PathParam("userType")@UserTypeStringuserType,Useruser);}在上面的简要代码中,我们可以发现有四个自定义注解,@Group、@Permission、@UserId和@UserType。@Permission,为每个API(接口方法)定义一个权限,需要name(英文格式)、label(中文格式)和description(权限说明)。@Group,即定义的权限属于哪个权限组。考虑到一个接口包含很多API,接口的数量比较多,那么我们可以将每个接口下的所有方法归为一组。业务服务可以定义自己的权限组,也可以选择不定义,都属于默认预定义的权限组。@UserId,即业务服务需要在其API中添加用户ID参数。AOP切面在拦截并进行授权验证时,用户ID是一个必须的参数,需要传入@UserType,即业务服务需要在自己的API中添加用户类型参数。AOP切面在拦截和进行权限验证时,用户类型是一个必须传入的参数,用户类型使得同一个业务服务可以支持多个用户系统。权限入库和拦截API权限定义完成后,我们在权限组件中添加扫描权限入库和拦截的算法。SpringAutoProxy自动代理的框架就是用来实现我们的扫描算法的。实现Objectinvoke(MethodInvocationinvocation)方法获取注解值。根据不同的注解拦截不同的方面,实现@Group、@Permission、@UserId和@UserType四个注解的权限拦截逻辑创建PermissionAutoProxy.java,继承Spring的org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator类,步骤为如下:在构造方法中设置Interceptor总代理(即实现MethodInterceptor接口的拦截类PermissionInterceptor.java)。shouldProxyTargetClass用于判断是接口代理还是类代理。在定义权限的时候,其实我们也支持在实现类上添加注解,而不仅仅是在接口上,这样可以灵活使用注解的方式。getAdvicesAndAdvisorsForBean是核心方法,用于确定应该将哪个类和哪个方法上的注解扫描进库,以及代理哪个类和哪个方法。如果再通用一点,我们可以抽象出三个方法供getAdvicesAndAdvisorsForBean调用。//返回拦截类,拦截类必须实现MethodInterceptor接口,即PermissionInterceptorprotectedabstractClassgetInterceptorClass();//返回接口或类的方法名上的注解,如果接口或类中的方法名上存在注解,则认为接口或类需要被代理protectedabstractClassgetMethodAnnotationClass();//扫描接口或类的方法名上的注解后,必须进行处理protectedabstractvoidmethodAnnotationScanned(ClasstargetClass);创建PermissionScanListener.java来实现Spring的组织。springframework.context.ApplicationListener.ApplicationListener接口,步骤如下实现onApplicationEvent(ContextRefreshedEventevent)方法中的存储代码。当业务服务的Spring容器启动时,会自动触发权限数据存储事件。通过上面的描述,我们实现了权限的扫描、存储和拦截。您可以参考下面的流程图。API权限对应的角色(Role)管理角色是一组API权限的概括。每个角色也会与相关的微服务名称挂钩。角色组的作用是对很多角色进行归纳和管理。角色管理需要在界面上进行手动操作。角色管理分为角色组的添加、删除、修改、查看,以及每个角色组下的角色的添加、删除、修改、查看。(角色组管理页面)(角色管理页面)API权限所属角色与用户User的绑定权限不能直接与用户绑定,必须通过角色作为中间桥梁进行关联。那么我们要实现:角色与权限的绑定,即一个角色与多个权限的关联,用户与角色的绑定,即一个用户与多个角色的关联(角色和权限的绑定页面)permissions)(rolesandpermissionsBindingpage)权限系统验证方式API访问验证方式是通过远程RPC调用,即通过权限API注入进行远程调用。Rest的认证方式通过UserID,UserType,PermissionName(权限名,mapping为对应的方法名)、PermissionType(区别是API权限或接口权限)、ServiceName(应用名)来判断是否授权,返回结果为true或false。权限服务与用户服务的集成用户服务是LDAP系统用户与桥接业务用户系统的集成。权限服务接入用户服务后,可以在权限授权页面选择相应的用户进行权限授权。权限服务与Redis分布式缓存系统的集成由于权限服务是公共服务,它提供了对碧蛋侠所有业务服务的权限访问,因此性能压力会非常大。我们通过使用Redis分布式缓存系统缓存权限进行验证。但是,有一个策略需要注意。当增加或删除与用户相关的角色,或改变其所属的绑定关系时,需要将缓存中的权限数据作废并删除。目前存在的问题和未来规划中的问题:由于访问权限服务的业务微服务数量不够多,随着后期访问量的增加,可能会暴露出更多的问题,比如高并发要求和低延迟请求等待。UserID和UserType参数都必须添加到业务权限API中,这给他们带来了一些不适。希望以后的版本可以通过预购来解决。对于未来的规划,我们将考虑通过各种机制来实现服务级别的访问控制:黑/白IP列表机制。当服务A调用服务B时,??服务B会实现并维护一个黑白IP列表,表示服务B只允许某个IP网段的服务A有调用服务B的权限。服务之间约定SecretKey来实现安全访问。当A服务调用B服务时,两个服务之间约定了API访问密钥,这个密钥不能轻易泄露。这样可以防止通过模拟Rest请求(例如,通过PostMan)来调用B服务。服务的API签名。当服务A调用服务B时,??服务A需要获取服务B正确的API签名才能被授权调用。笔者介绍,任浩军现任上海勤仓(支付丹霞)信息技术有限公司基础设施部架构师,毕业于浙江大学。加入勤仓之前,曾在朗讯、惠普、泰克、快钱等公司工作。专注于JavaCore、基于NIO(Netty)的分布式RPC框架、MQ、SpringCloud等相关领域,积累了丰富的互联网架构设计经验,具有丰富的开源项目和精神。
