买单侠任浩军:微服务API级权限的技术架构
背景:权限是根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。在实际的生产系统中,用户数量十分庞大,权限的划分需要结合具体的业务场景,一旦把控不住力度,工作十分繁重,如何解决这个问题?
例如,对于一个文件系统的权限来说,用户A和B只具有查看和拷贝该文件系统下某些文件的权限,而用户C和D不仅有查看和拷贝文件的权限,也具有修改和删除文件的权限,这些权限的划分和授权需要事先通过专门管理员进行操作。
业界专门提出了一套权限模型和方法,RBAC(Role-Based Access Control),即基于角色(Role)的访问控制方法。它的核心概念如下:
角色(Role)与权限(Permission)相关联,一个角色对应多个权限
用户(User)与角色(Role)相关联,一个用户对应多个角色
权限(Permission)包含资源,或者与操作组合方式相结合
这样,我们就实现了让用户通过成为适当角色的成员而得到这些角色的权限,最终实现权限控制的目的。
结合上述的文件系统例子,我们可以去用RBAC去刻画和描述:
文件系统中的文件是权限概念中的“资源”,对文件的删除是“操作”,那么我们可以定义出一个“文件删除”的权限来。
访问文件系统的用户A、B、C和D即上述模型中的用户,当然如果用户很多,我们可以划分出“用户组”概念。
对于角色,我们可以划分出两个角色,第一个是“文件普通用户”角色,它包含“文件查看”和“文件拷贝”两个权限;第二个是“文件管理员用户”角色,它包含“文件修改”和“文件删除”两个权限。
一言以蔽之,基于角色的访问控制方法的访问逻辑表达式为“Who对What(Which)进行How的操作”,它的由内到外的逻辑结构为权限->角色->用户,即一个角色对应绑定多个权限,一个用户对应绑定多个角色。
从图中可以看到,用户A和B赋予了“文件普通用户”角色,即他们拥有了“文件查看”和“文件拷贝”的权限;
用户C和D同时赋予了“文件普通用户”和“文件管理员用户”的两个角色,即他们拥有了“文件查看”、“文件拷贝”、“文件修改”和“文件删除”。
如果后面,我们觉得“文件拷贝”有文件泄密的安全问题,那么只需要从它从“文件普通用户”角色移除就可以了,上述4个用户自然无法实行对文件拷贝的这个操作了,所以RBAC模型对于权限扩展和收缩非常方便。
阐述完权限系统的基本概念后,我们来讲讲,权限系统在互联网时代的分布式系统中,尤其是微服务架构的体系下,有什么样的挑战?它又必须解决哪些问题,最适合采用什么框架和技术去解决这些问题?
现状及挑战微服务架构下的现状
服务实例数量庞大
目前,组成买单侠业务线系统,有将近400多个微服务,我们知道微服务的优点是可以清晰的划分出业务逻辑来,让每个微服务承担职责单一的功能,毕竟越简单的东西越稳定。
但是,微服务也带来了很多的问题,完成一个业务操作,需要跨很多个微服务的调用,那么如何用权限系统去控制用户对不同微服务的调用,对我们来说,是个挑战。
用户系统数量多类型复杂
目前,接入买单侠业务线的用户系统数量多类型复杂,且数据分散,比如有公司的员工系统(LDAP系统),公司的销售人员系统,公司的外包人员系统,外部互联网用户系统(使用APP的客户)。
不同类型的用户系统都有可能接入某些微服务,那么如何用权限系统去控制不通用户对同一个微服务的调用,对我们来说,又是一个挑战。
微服务吞吐量大 可用性要求高
当业务微服务的调用接入权限系统后,不能拖累它们的吞吐量,当权限系统出现问题后,不能阻塞它们的业务调用进度,当然更不能改变业务逻辑。
已有业务系统快速接入权限
新的业务微服务快速接入权限系统相对容易把控,那么对于公司已有的微服务,如何能不改动它们的架构方式的前提下,快速接入,对我们来说,也是一大挑战。
技术方案及系统架构
经过不断的业界框架的选型对比,原本想采用Spring Security框架来做我们的权限框架。
但是经过研究,它有很多优势,但也有明显的两个不足:框架笨重,权限数据持久化层次结构不好,最重要的是无法做数据库持久化。
于是我们决定自研开发,技术方案有几个特点:
权限系统微服务化
既然有那么多微服务,那么我们把权限系统也微服务化,通过微服务来控制其他微服务的权限,保证整体系统架构的一致性。
微服务的统一性和独立性
未来买单侠所有的业务微服务都将接入到权限微服务中,做统一控制。
权限微服务即为独立的公共服务,作为众多微服务中的一员,它必定将遵循买单侠微服务架构的线路,即业务微服务的权限验证,要走阿里云SLB->Zuul API Gateway,如果是基于Web的权限验证,还需要套入Ngnix REST请求代理。
如下图所示:
(权限服务与其它公共服务的关系图)
业务微服务的代码微侵入
我们将采用自定义权限的注解(Annotation),尽可能增加新的代码到业务代码层面,减轻业务线的负担。
高可用,分布式的权限缓存
基本权限/角色数据我们通过MySQL的数据中,权限验证数据,则通过Redis集群缓存。
那么意味着,对于足够多的权限验证数据缓存Redis集群后,权限微服务全部崩溃也没关系;反之,当Redis集群崩溃,只要权限微服务运行正常,也不影响权限验证,只是性能会稍差而已。
支持多类型权限,多调用方式
我们将支持业务服务通过RPC方式进行权限验证,支持其他系统(例如Web)通过REST方式进行权限验证。
对于业务服务的,主要是支持接口加注解进行权限拦截验证,即API权限;对于其他系统,一般主要是体现在界面元素的权限校验(例如Web页面上按钮的Enabled/Disabled,通过权限系统来控制),即界面权限。
友好多维的权限/角色/用户
录入和绑定界面
权限数据导入导出
结合Spring OAuth单点登录,Spring Session等,实现安全体系范畴的权限扩展。
接下去,我们具体来阐述,权限微服务的核心技术方案,基于界面的权限控制相对容易,就略过了,主要讲一下基于API权限的实现。
API权限定义、入库和拦截
对于API权限,我们实现基于注解(Annotation)的扫描入库和拦截,不需要业务服务自行在权限Web界面上录入。
权限定义
API权限以每个接口或者实现类中的方法作为权限资源,每个权限和微服务名(Service Name)挂钩。
我们通过在业务服务的API上添加注解的方式,进行权限定义。基础架构部会提供一个权限组件(Permission Component)Jar给业务服务部门,里面包含了自定义的注解,这样的实现方式,对业务服务的影响非常小,增加权限机制只是在代码层面加几个注解而已。具体使用方式如下。
对于一个普通的接口类,可以这样定义:
@Group(name = "User Permission Group", label ="用户权限组", description = "用户权限组")
public interface UserService {
@Permission(name = "AddUser", label = "添加用户")
boolean addUser(@UserId StringuserId, @UserType String userType, User user);
}
对于通过Swagger方式暴露出去的API,可以这样定义:
@Path("/user")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Api(value = "User resource operations")
@Group(name = "User Permission Group", label ="用户权限组", description = "用户权限组")
public interface UserService {
@POST
@Path("/addUser/{userId}/{userType}")
@Permission(name = "AddUser", label = "添加用户")
booleanaddUser(@PathParam("userId") @UserId String userId, @PathParam("userType")@UserType String userType, User user);
}
在上述简短的代码中,我们可以发现有四个自定义的注解,@Group、@Permission、@UserId和@UserType。
@Permission,即为每个API(接口方法)定义一个权限,要求有name(英文格式),label(中文格式)和description(权限描述)。
@Group,即定义的权限归属哪个权限组,考虑到一个接口中包含很多个API,接口数目又比较多,那么我们可以为每个接口下的所有方法归为一个组。业务服务可自行定义权限组,也可以选择不定义,那么会归属到默认预定义的权限组中。
@UserId,即业务服务需要在他们的API上加入用户ID的参数,当AOP切面拦截做权限验证时候,用户ID是需要传入的必要参数。
@UserType,即业务服务需要在他们的API上加入用户类型的参数,当AOP切面拦截做权限验证时候,用户类型是需要传入的必要参数。用户类型可以让同一个业务服务支持多个用户系统。
权限入库和拦截
当API权限定义好以后,我们在权限组件里面加入扫描权限入库和拦截的算法。采用Spring AutoProxy自动代理的框架来实现我们的扫描算法。
实现Object invoke(MethodInvocation invocation)方法,获取注解值。
根据不同注解进行不同的切面拦截,实现对@Group,@Permission、@UserId和@UserType四个注解的权限拦截逻辑
创建PermissionAutoProxy.java,继承Spring的org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator类,步骤如下:
在构造方法里设置好Interceptor通用代理器(即实现了MethodInterceptor接口的拦截类PermissionInterceptor.java)
shouldProxyTargetClass用来决定是接口代理,还是类代理。在权限定义的时候,其实我们还支持把注解加在实现类上,而不仅仅在接口上,这样灵活运用注解放置的方式。
getAdvicesAndAdvisorsForBean是最核心的方法,用来决定哪个类、哪个方法上的注解要被扫描入库,也决定哪个类、哪个方法要被代理。
如果我们做的更加通用一点,那么可以抽象出三个方法,供getAdvicesAndAdvisorsForBean调用。
// 返回拦截类,拦截类必须实现MethodInterceptor接口,即PermissionInterceptor
protected abstract Class getInterceptorClass();
// 返回接口或者类的方法名上的注解,如果接口或者类中方法名上存在该注解,即认为该接口或者类需要被代理
protected abstract Class getMethodAnnotationClass();
// 扫描到接口或者类的方法名上的注解后,所要做的处理
protected abstract voidmethodAnnotationScanned(Class targetClass);
创建PermissionScanListener.java,实现Spring的org.springframework.context.ApplicationListener.ApplicationListener
onApplicationEvent(ContextRefreshedEvent event)方法里实现入库代码。 在业务服务的Spring容器启动的时候,将自动触发权限数据入库的事件。 通过上述阐述,我们就实现了权限的扫描入库和拦截,可以参照下面的流程图 API权限所对应的角色(Role)管理 角色是一组API权限的汇总,每个角色也将和微服务名挂钩。角色组的作用是为了汇总和管理众多的角色。 角色管理需要人工在界面上进行操作,角色管理分为角色组增删改查,以及每个角色组下的角色增删改查。 (角色组管理页面) (角色管理页面) API权限所属角色和用户User的绑定 权限不能直接和用户绑定,必须通过角色作为中间桥梁进行关联。那么我们要实现: 角色与权限的绑定,即一个角色和多个权限的关联 用户与角色的绑定,即一个用户和多个角色的关联 (角色和权限的绑定页面) (角色和权限的绑定页面) 权限系统验证方式 API接入的验证方式 通过远程RPC方式的调用,即通过权限API的方式注入,进行远程调用。 Rest调用的验证方式 http://host:port/authorization/authorize/{userId}/{userType}/{permissionName}/{PermissionType}/{serviceName} 通过User ID、User Type、Permission Name(权限名,映射于对应的方法名)、Permission Type(区别是API权限还是界面权限),Service Name(应用名)来判断是否被授权,返回结果是true或者false。 权限服务和用户服务的整合 用户服务即整合了LDAP系统的用户和桥接业务用户系统。 权限服务接入用户服务后,可以在权限授权页面上选取相应的用户进行权限授权。 权限服务 和Redis分布式缓存系统整合 由于权限服务属于公共服务,它提供面向买单侠所有业务服务的权限接入,所以承受的性能压力会很大,我们通过运用Redis分布式缓存系统缓存已经验证过的权限。 但其中需要注意一个策略,当跟某个用户有关的角色,权限添加删除,或者所属的绑定关系发生变更的时候,需要让缓存中的权限数据失效和删除。 目前存在的问题及未来规划 存在的问题: 由于接入权限服务的业务微服务数量还不够多,随着后期接入数量的增加,可能会有更多问题暴露出来,比如高并发要求、低延迟要求等等。 业务权限的API上都要加User ID和User Type两个参数,给他们带来些许的不适,期望未来的版本能通过前置埋点的方式解决。 未来的规划,我们会考虑通过多种机制实现服务级别的访问控制: 黑/白IP名单机制。当A服务调用B服务的时候,B服务会实现维护一个黑/白IP列表,表示B服务只允许在某个IP网段的A服务才能有权限调用B服务。 服务间约定SecretKey实现安全访问。当A服务调用B服务的时候,两个服务之间实现约定API访问密钥,此密钥不能轻易泄密。这样就规避了B服务被模拟Rest请求调用(例如通过PostMan调用)。 服务的API签名。当A服务调用B服务的时候,A服务需要获得正确的B服务API的签名,才有权限去调用。 作者介绍 任浩军 现任上海秦苍(买单侠)信息科技有限公司基础架构部架构师,毕业于浙江大学,加入秦苍之前,曾在朗讯、惠普、泰克、快钱等公司任职。 专注于Java Core 、基于NIO(Netty)的分布式RPC框架、MQ、Spring Cloud等相关领域,积累了丰富的互联网架构设计经验,具有丰富的开源项目和精神。 目前主要负责秦苍公共服务,安全领域相关技术。