spring boot / cloud (十四) 微服务间远程服务调用的认证和鉴权的思考和设计,以及restFul风格的url匹配拦截方法
前言
本篇接着
<spring boot / cloud (八) 使用RestTemplate来构建远程调用服务>
这篇博客来继续讨论
微服务间接口调用的认证和鉴权的思考和设计
在上一篇文章中主要是偏实现方面,具体的实现思想没有过多讨论,本篇文章则是主要讨论一下设计的思路.
我们都知道,在微服务的架构设计中,一个大的系统会被按照不同的领域拆分成一个个小的微服务,而这些微服务之间不可避免的会有业务数据的交互,
那么我们会用一些远程服务调用的方式来连接各个微服务( 比如:RPC,RestFul,等 ).
不过,就像前面说的一样,期初,应用不大,随便弄弄无所谓,但是等应用规模起来以后,你会发现,有成百上千的服务在运行,这些服务相互依赖,仿佛一团乱麻.
更可怕的事情是如果没有有效的权限控制,我们很有可能都不清楚是谁调用了你的服务……
所以说,我认为,在微服务架构设计中,内部服务调用的权限控制是非常必要的(至少我参与的项目都有这种需求),它应该满足如下几个主要的功能:
-
防止越权行为
-
管理服务的依赖关系
-
规范服务调用行为
-
能够在运行时修改权限配置
下面我们来看看具体的分析:
场景分析
防止越权行为
在系统中添加权限相关的控制,主要是为了增加系统的安全性,总结下来主要是为了防止如下的两种越权行为:
-
横向越权 (指的是攻击者尝试访问与他拥有相同权限的用户的资源)
-
纵向越权 (指的是一个低级别攻击者尝试访问高级别用户的资源)
所以说通常,在系统中的权限校验也按照以上划分会分为两个步骤:
-
校验访问者身份
-
校验是否拥有被访问资源的权限
校验访问者身份
先说
校验访问者身份
,这个主要目的就是确定A的确是A,不是其他的阿猫阿狗.
要做到这一点并不难,并且有很多安全框架都能支持,比如apache shiro,spring security,jwt,等等.
这些框架主要思路还是使用token签名的方式,也就是说,
要么调用方和服务方约定一个私钥,然后调用方自行通过算法生成token,
或者服务方提供一个获取token的接口(OAuth2),调用方主动调用接口获取token,
最后调用方在调用服务的时候,都把这个token给带上,便于服务方认证身份.
那么那种方式更好呢? 个人经验我会按场景做如下架构原则定义:
-
如果服务是给系统用的,则采用私钥的方式
-
如果服务是给用户(人)使用的,则采用获取token的方式(通过用户名和密码来获取token)
那么,还有一种情况,如果这个接口既是给人使用的,也是给第三方系统使用的,怎么办呢?
这个其实也不复杂,不过不会在今天这篇文章中讨论,这里只提一点,
通过入口区分,也就是网关
,大家可先自行脑洞.
那么,回过头来看,我们今天讨论的场景显然是属于是给系统使用的服务,所以在我设计的RMS组件中,是采用的私钥的方式.
采用这种身份认证方式需要注意如下几点:
-
私钥的安全性
-
token的过期策略
-
token的计算算法
在RMS组件中,我并没有引入第三方的依赖,因为我希望,这个身份认证是轻量级的,灵活的,这些第三方认证框架大而全,很优秀,但我们只会用到其中的一小块,会造成一些没必要的依赖.
从实现方面,首先,所有的私钥,都会配置到远端的配置中心里面,本地不做任何存储,由专门的人员管理和维护,系统只有在运行的时候,才能获取到私钥.
同时依赖于spring cloud config server的特性,可以在运行时更换私钥,更加灵活,也保证了的私钥的安全,
如下的sign(token)的算法,通过应用名称和私钥,要有当前时间(精确到小时),拼接起来然后进行md5,得到最终的sign,因为加入了时间的这个因子,所以计算出来的sign是每小时过期的
算法方面大家可以随意设计,但是切记,不要过度设计,满足需求即可
public static String sign(String rmsApplicationName, String secret) {
final String split = "_";
StringBuilder sb = new StringBuilder();
sb.append(rmsApplicationName).append(split).append(secret).append(split)
.append(new SimpleDateFormat(DATA_FORMAT).format(new Date()));
return DigestUtils.md5Hex(sb.toString());
}
校验是否拥有被访问资源的权限
然后我们再聊
校验是否拥有被访问资源的权限
,这个点说简单也简单,说复杂也非常复杂.
在前面一步的校验中,已经确定了身份,现在是要确定,A是否有访问B的/user服务的权限.
其他的不说,我这边只提两点:
-
uri匹配
-
性能
在没有RestFul风格的url的时候,一切其实都还蛮美好的,因为,url就是唯一值,是整个系统的最小颗粒度的权限点.
大家以前可能是这样做的,有张表,记录这系统的url,以及其他的角色,岗位等的关联,然后,如何校验呢,非常简单,
直接的sql语句select count(1) form xxx where url=’/aaa/getUserByName’就行了,能查到值就代表有权限.
在稍微进阶一点的,会考虑性能问题,会将某个用户的一些权限缓存起来,然后在内存中进行判断.
但是,当RestFul风格的url到来的时候,这一切变得不那么美好了,先看如下几个url的例子:
GET /users -- 查询用户列表
GET /user/{id} -- 查询用户详情
POST /user -- 新增用户
PUT /user --更新用户
DELETE /user/{id} --删除用户
GET /user/{id}/scores -- 查询某个用户的所有成绩
GET /user/{id}/score/{sid} -- 查询某个用户的某门课程的成绩
我们可以看到,按照原有的方式已经不那么使用了,url的定义从原来的平面化的,变成了立体化的,
按照原来的方式,那么就变成了,如果拥有查询用户详情接口权限的系统,同时也就拥有了更新用户和删除用户的权限,这是非常严重的越权行为.这显然不是我们期望看到的.
那么如何优化呢? 首先,我们的权限判断中应该加入
httpmethod
的判断,这样,就能很简单的避免以上的情况.
但是更严重的问题来了,url不再是固定不变的了,而是动态的,怎么办呢?先拍脑子想想,处理方案可能有如下几种:
-
正则匹配
-
将所有url解析成树形结构,将动态部分用星号表示,然后进行最短匹配
以上两种方案我都试过,不过方案都过于复杂,甚至存在性能问题,因为以上两种方式都不可不免会进行循环匹配.
我们当然不想因为一个url校验,而引入一个性能问题的风险,那么如何解决这个问题呢?
其实我们回过头来想想,spring mvc为什么就能准确的定位到每个url对应的handler呢?
其实还是那句话,最复杂的部分,spring已经帮我们完成了,在spring 上下文初始化的时候,容器就会记录所有的mapping,如下:
Mapped "{[/health || /health.json],methods=[GET],produces=[application/json || application/json]}" onto HealthMvcEndpoint.invoke(HttpServletRequest,Principal)
Mapped "{[/env/{name:.*}],methods=[GET],produces=[application/json || application/json]}" onto EnvironmentMvcEndpoint.value(String)
Mapped "{[/env || /env.json],methods=[GET],produces=[application/json || application/json]}" onto EndpointMvcAdapter.invoke()
Mapped "{[/features || /features.json],methods=[GET],produces=[application/json || application/json]}" onto EndpointMvcAdapter.invoke()
以上日志大家随便启动一个spring boot应用都能看到,其实这类输出就是我们controller定义的requestMapping
@RequestMapping(value = "/user/{id}/score/{sid}", method = RequestMethod.GET)
而我们平时获取url的方式是这样的:
String url = request.getRequestURI();
这样获取到的url 大概是,也就是我们实际请求的url:
/user/1/score/5
那么如何改变呢?以上这个url我们还是不知道如何匹配? 换个思路想想,能进入拦击器,则表示spring将这个地址已经匹配到了对应的handler,我们只需找到这个url对应的那个handler就行了.
在request的作用域中(Attribute),存放这很多spring的信息,debug一下,打个断点,看看都有啥?,最终我定位到了bestMatchingPattern这个属性,大致含义就是最佳匹配模式,也里面的值是requesMapping里的value
这不正是我们想要的结果吗?spring已经帮我们做了最佳的替换,如下代码:
String url = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).toString();
//url的值为 : /user/{id}/score/{sid}
那么后续我们就可以调整我们的设计了,如果你的权限配置是配置在数据库中的那么,最简单的鉴权sql语句就是:
select count(1) form xxx where url='/user/{id}/score/{sid}' method='GET'
这样做的好处就是可以做到最精确的匹配,颗粒度达到最细,并且毫无性能损耗,你无需做任何的循环匹配.详细代码可以在项目的RmsAuthHandlerInterceptor类中找到
其他
在上面还提到了
管理服务的依赖关系
,
规范服务调用行为
,
能够在运行时修改权限配置
这几点 ,
在上一篇讲解RMS代码的时候也都提及到了,会通过远程配置文件的方式,来进行管理,如下:
# application
org.itkk.rms.properties.application.udf-service-a-demo.serviceId=UDF-SERVICE-A-DEMO
org.itkk.rms.properties.application.udf-service-a-demo.secret=ASD5S2SDF6ASD2S2SD32S
org.itkk.rms.properties.application.udf-service-a-demo.purview=ID_2,SCHEDULER_JOB_4,SCH_CLIENT_CALLBACK_1
org.itkk.rms.properties.application.udf-service-a-demo.all=false
org.itkk.rms.properties.application.udf-service-a-demo.disabled=false
org.itkk.rms.properties.application.udf-service-a-demo.description=测试服务A
# service
org.itkk.rms.properties.service.ID_1.owner=udf-general-server-demo
org.itkk.rms.properties.service.ID_1.uri=/service/id
org.itkk.rms.properties.service.ID_1.method=GET
org.itkk.rms.properties.service.ID_1.isHttps=false
org.itkk.rms.properties.service.ID_1.description=获得分布式ID
会分为application和service两类,application主要描述身份认证和权限,service主要描述服务详情 . 通过这种结构来管理服务间的依赖关系
然后在项目中,大家可以看Rms这个类,里面抽象出了一个公共的方法,用于规范调用行为,最终调用的方式如下:
ResponseEntity<RestResponse<FileInfo>> fileInfo = rms.call("FILE_4", fileParam, null,
new ParameterizedTypeReference<RestResponse<FileInfo>>() {
}, null);
在系统中,开发人员都无需关心任何接口的定义,只需通过接口编号就可以进行调用.
最后,所有的RMS相关的配置都会放在配置中心,同一管理.
结束
今天代码层面的东西讲的比较少,主要是跟大家介绍一下设计的思路,还是一个原则,使代码更健壮,更灵活,更合理,同时,也切记不要重复造轮子,也不要过度玩技术.
在下一篇文章中,我会介绍一下分布式任务调度的思考和设计,敬请期待.
代码仓库
(博客配套代码)
想获得最快更新,请关注公众号
转载于:https://my.oschina.net/wangkang80/blog/1524746