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相关的配置都会放在配置中心,同一管理.

结束

今天代码层面的东西讲的比较少,主要是跟大家介绍一下设计的思路,还是一个原则,使代码更健壮,更灵活,更合理,同时,也切记不要重复造轮子,也不要过度玩技术.

在下一篇文章中,我会介绍一下分布式任务调度的思考和设计,敬请期待.

代码仓库 (博客配套代码)


想获得最快更新,请关注公众号

spring boot / cloud (十四) 微服务间远程服务调用的认证和鉴权的思考和设计,以及restFul风格的url匹配拦截方法的更多相关文章

  1. spring boot / cloud (十六) 分布式ID生成服务

    spring boot / cloud (十六) 分布式ID生成服务 在几乎所有的分布式系统或者采用了分库/分表设计的系统中,几乎都会需要生成数据的唯一标识ID的需求, 常规做法,是使用数据库中的自动 ...

  2. spring boot / cloud (十五) 分布式调度中心进阶

    spring boot / cloud (十五) 分布式调度中心进阶 在<spring boot / cloud (十) 使用quartz搭建调度中心>这篇文章中介绍了如何在spring ...

  3. spring boot / cloud (八) 使用RestTemplate来构建远程调用服务

    spring boot / cloud (八) 使用RestTemplate来构建远程调用服务 前言 上周因家里突发急事,请假一周,故博客没有正常更新 RestTemplate介绍: RestTemp ...

  4. spring boot / cloud (十八) 使用docker快速搭建本地环境

    spring boot / cloud (十八) 使用docker快速搭建本地环境 在平时的开发中工作中,环境的搭建其实一直都是一个很麻烦的事情 特别是现在,系统越来越复杂,所需要连接的一些中间件也越 ...

  5. spring boot / cloud (十九) 并发消费消息,如何保证入库的数据是最新的?

    spring boot / cloud (十九) 并发消费消息,如何保证入库的数据是最新的? 消息中间件在解决异步处理,模块间解耦和,和高流量场景的削峰,等情况下有着很广泛的应用 . 本文将跟大家一起 ...

  6. Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务

    Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的云应用开发工具:Spr ...

  7. spring boot / cloud (十二) 异常统一处理进阶

    spring boot / cloud (十二) 异常统一处理进阶 前言 在spring boot / cloud (二) 规范响应格式以及统一异常处理这篇博客中已经提到了使用@ExceptionHa ...

  8. Spring Boot 和 Docker 实现微服务部署

    Spring boot 开发轻巧的微服务提供了便利,Docker 的发展又极大的方便了微服务的部署.这篇文章介绍一下如果借助 maven 来快速的生成微服务的镜像以及快速启动服务. 其实将 Sprin ...

  9. Spring boot学习1 构建微服务:Spring boot 入门篇

    Spring boot学习1 构建微服务:Spring boot 入门篇 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框 ...

随机推荐

  1. Linux C编程学习之开发工具3---多文件项目管理、Makefile、一个通用的Makefile

    GNU Make简介 大型项目的开发过程中,往往会划分出若干个功能模块,这样可以保证软件的易维护性. 作为项目的组成部分,各个模块不可避免的存在各种联系,如果其中某个模块发生改动,那么其他的模块需要相 ...

  2. UWP开发入门(二十二)——Storyboard和Animation

    微博上有同学问我MyerSplash是如何实现那个很炫的图片点亮,然后移动到屏幕中央的效果.惭愧啊,我又不是作者哪里会知道.硬着头皮去GitHub拜读了高手的代码,自愧弗如,比我不知道高到哪里去了…… ...

  3. RDIFramework.NET开发实例━表约束条件权限的使用-WinForm

    RDIFramework.NET开发实例━表约束条件权限的使用-WinForm 在实际的应用中,客户常有这样的需求,指定用户或角色可以看指定条件下的数据,这里的“指定条件”在RDIFramework. ...

  4. noi 6045 开餐馆

    题目链接:http://noi.openjudge.cn/ch0206/6045/ 解题报告:参考了konjac 蒟蒻的. 题意: 有N个地址,从中选一些开餐馆,要保证相邻餐馆的距离大于k.问最大利润 ...

  5. Bootstrap-select:美化原生select

    官网:http://silviomoreto.github.io/bootstrap-select/ 1.下载zip 2.html代码 <select class="selectpic ...

  6. codeforces 377A. Puzzles 水题

    A. Puzzles Time Limit: 20 Sec  Memory Limit: 256 MB 题目连接 http://codeforces.com/problemset/problem/33 ...

  7. MySQL中SELECT语句简单使用

    最近开始复习mysql,查漏补缺吧. 关于mysql 1.MySQL不区分大小写,但是在MySQL 4.1及之前的版本中,数据库名.表名.列名这些标识符默认是区分大小写的:在之后的版本中默认不区分大小 ...

  8. Java如何计算一个程序的运行时间

    话不多说 直接看代码 package com.mowcode; /** * * @ClassName: Code_01_ProjectTime * @Description: 拿到程序运行时间 * @ ...

  9. 记录一次JQuery 动态参数使用

    之前动态id 使用时时候一直是复制黏贴的,到自己实际使用的时候却毫无印象,这样不可取呀 $('#id')  取的是 id=‘id’ 的元素 $('#'+id)  才是取id为动态值的时候正确写法 记录 ...

  10. Sublime Text 3 Mac常用快捷键与注意事项

    大多数情况下容易忘记的快捷键,在此整理了一下. 编辑快捷键:cmd+L:选择行(重复按下将下一行加入选择):cmd+D:选择词(重复按下时多重选择相同的词进行多重编辑):cmd+shift+D 复制光 ...