微服务 ,无论做什么,都是去年发明的最重要的概念之一。抵制 SOAP 2.0 的时间有多长是可能的,但迟早他们会来找你,把你变成他们的信仰,或者你会来找他们,请用火和剑给自己洗礼。与任何架构概念一样,微服务也有缺点。您需要在来自外部系统或每个微服务中的其他微服务的请求中包含一些授权逻辑。这个逻辑可以直接“硬编码”在微服务中(并且是一个单独的库并不重要),或者可以委托给其他微服务,或者可以声明。 “可以申报”是什么意思?例如,可以同意特殊的 HTTP 标头,或一些带有用户信息的数据结构,出现在对每个微服务的每个请求中。并且需要绝对信任这种结构中的数据。这三个选项都有缺点,但在文章中我们将讨论最后一个。对于实现,通常使用 API 网关 模式:
通常,API 网关会限制对内部服务的请求数量、授权客户端的请求、进行日志记录和审计、跨客户端分发请求并在必要时转换数据。甚至 nginx 也可以用于 API 网关。考虑用户请求的授权功能。如果使用 HTTP 协议,标准做法会考虑在 Authorization 标头中添加某个令牌(并不重要,因为我们收到它):
Authorization: Bearer <some token>
在网关端,检查此标头,然后将标头交换到另一个标头,该标头包含令牌写入的用户的知识,例如其标识符。这个另一个令牌将被转发到内部微服务:
Authorization: Bearer <some token>
一切看起来简单明了,但问题是 Apache Thrift 像馅饼一样由几个部分组成:
Authorization: Bearer <some token>
一般来说,我们不会卡在协议或传输上。当然可以选择一个。我们可以同意我们只使用 HTTP,但它限制了传输转换的机会,并且它强制在微服务中执行某些外部处理器/过滤器(HTTP 标头不是 Thrift 的本机)。
此时此刻,一个疯狂的想法浮现在脑海中:如果在通过我们的网关传递请求时使用协议的可能性将外部授权令牌替换为内部令牌怎么办?
约定优于配置
好的,让我们有这样的服务:
Authorization: Bearer <some token>
UserData 有一些关于用户的信息。根据它,微服务返回特定用户的数据。而且(我想你明白)不能从外部世界调用此服务。但有什么可能呢?也许这个:
Authorization: Bearer <some token>
如您所见,第一个参数中的两个服务与未授权异常之间的差异为 99 字段(我希望没有人需要超过 98 个异常 :))。所以我们只需要将外部令牌替换为内部令牌即可。
内脏
不幸的是,Thrift 的文档非常稀少。所有指南,包括 其中最好的 指南,都不涉及内部协议的实现。悲伤,但它很清楚。在 99% 的情况下,开发人员不需要知道这一点,但我们需要。
共有三种最流行的协议:
- 二进制 - 只是二进制(字符串,例如,按 UTF-8 传输)
- 紧凑 - 二进制但更紧凑
- JSON - 非常具体的 JSON
它们每个都有自己的实现,由 API 封装。从 API 的角度来看,二进制数据包如下所示:
TMessage - 关于消息的元信息。由名称、方法名、方法在服务中的类型序号组成。类型可能是:
- CALL = 1 - 传入消息
- 回复 = 2 - 结果
- 异常 = 3 - 无评论
- ONEWAY = 4 - 对于 void 方法
其他是有效载荷,即在传入消息结构中打包。
所有呈现的协议逐字节读取数据数组并存储其当前索引以从正确的位置继续读取。
所以我们需要如下算法:
- 读TMessage
- 读取消息结构开始
- 读取消息中第一个字段的元数据
- 存储当前数组索引
- 读取令牌
- 存储当前数组索引
- 用户信息交换令牌
- 序列化用户信息
-
创建包含三个部分的新数组:
- 从项目 4 开始到存储的索引
- 第 8 项中的序列化用户信息
- 从第 6 项的存储索引到消息结束
测试它
没有测试,没有代码。所以先写测试。对于测试,我们需要以下服务:
Authorization: Bearer <some token>
使用数据创建和填充外部服务:
Authorization: Bearer <some token>
TMemoryBufferWithLength - 我们需要创建新类,因为 TMemoryBuffer 有致命缺陷。它不能返回实际的数组长度。取而代之的是,它返回的缓冲区长度可以超过消息长度,因为一些字节是保留的。
方法 send_getSomeData 将消息序列化到缓冲区中。
我们对内部服务做同样的事情:
Authorization: Bearer <some token>
接下来我们用序列化数据创建字节数组:
Authorization: Bearer <some token>
最后,我们创建了将外部消息转换为内部消息的主类: MessageTransalator 。
Authorization: Bearer <some token>
令牌交换实现( AuthTokenExchanger )取决于某个项目的需要,因此我们引入接口:
Authorization: Bearer <some token>
方法 createEmptyAuthToken 应该返回一个对象,即呈现空令牌。 MessageTransalator 稍后会填充它。在方法 过程 中,我们必须实现令牌交换。对于我们下面的简单测试实现就足够了:
Authorization: Bearer <some token>
然后添加断言:
Authorization: Bearer <some token>
运行测试,但它们不起作用。真是太好了!
绿灯
根据算法实现 处理 方法:
Authorization: Bearer <some token>
我们使用 TMemoryInputTransport 传输,可以直接从输入缓冲区读取。
Authorization: Bearer <some token>
实现查找令牌数据边界的方法:
Authorization: Bearer <some token>
序列化用户数据:
Authorization: Bearer <some token>
运行测试,然后...
夏洛克
因此,二进制和紧凑协议的测试通过了,但 JSON 没有通过。出了什么问题?调试并查看比较的数组之间的差异:
Authorization: Bearer <some token>
没看出区别?但它是。在第一个 “rec” 符号“:”被遗漏之后。我们使用一个 API 但看到不同的结果。只有在仔细阅读类 TJSONProtocol 的代码后,解决方案才会出现。它有领域:
Authorization: Bearer <some token>
此上下文在处理 JSON 结构时将不同的分隔符存储在堆栈中。当它读取结构时,它读取符号“:”,但它不返回分隔符,因为我们的用户数据对象没有任何上下文。
在 seriaizeUserData 方法中手动添加符号:
Authorization: Bearer <some token>
然后运行测试,只看到绿色。
例外的兴起
这不是结束。如果授权失败,我们忘记了异常处理。 ok,在99位置加上unauthorize异常:
Authorization: Bearer <some token>
我们需要新方法 processError 。
Authorization: Bearer <some token>
Thrift 有两类异常,我们可以将它们序列化为输出消息。首先是隐式声明的 TApplicationException。其次是在服务定义的 抛出 部分中声明的自定义异常。因此,如果在授权时出现意外异常,我们应该使用 TApplicationException 创建消息。如果用户未被授权并且我们知道这一点,我们应该创建带有 UnauthorizedException 的消息。我们开始做吧。
Authorization: Bearer <some token>
一些评论。根据节俭协议,如果 TApplication 上升,我们应该使用 TMessageType.EXCEPTION 类型的消息。如果出现自定义异常 - TMessageType.REPLY 。
我们还需要在我们的翻译器中引入状态来存储我们在解析 TMessage 时应该填充的 methodName 和 seqid 。
例子
就这样。现在我们可以这样做:
Authorization: Bearer <some token>
MessageTranslator 的完整列表在 这里 。
链接
Github:https:
//github.com/aatarasoff/thrift-api-gateway-core
Bintray:
https://bintray.com/aatarasoff/maven/thrift-api-gateway-core/view
点心峰
在下一部分中,我们将在 Spring 堆栈上构建网关。