使用 Ballerina 构建 API 网关

阅读数:792 2018 年 7 月 31 日 17:58

关键要点

  • 在为 WSO2 API Manager 创建 API 网关时,我们实际上是创建了与 API Manager 上发布的 API 相关的 Ballerina 服务。
  • 请求过滤器可以访问传入的请求数据。因此,它可用于检查请求内容,对内容进行验证,复制并将请求推送到其他系统,甚至修改原始请求。
  • 默认情况下,客户端应用程序要调用网关上的服务需要发送由受网关信任的 STS(安全令牌服务)签名的令牌(JWT)。调用请求需要符合 OAuth Bearer 配置。
  • 你可以基于 Ballerina Streams 构建流式查询,对从事件流上接收到的数据进行投影、过滤、窗口、流连接和模式操作。
  • 在微服务架构中,用户需要生成 API 网关的容器运行时。在 Ballerina 中,我们可以在服务上使用相关的 Docker 和 Kubernetes 注解,从而简化了这一过程。

现代 API 是一种具有良好定义且易于理解的网络功能,可满足特定的业务需求。API 网关是架构模式中的一个层,负责请求分配、策略实施、协议转换和分析,让业务 API 专注于业务功能。

本文将介绍如何使用 Ballerina WSO2 API Manager 构建 API 网关。WSO2 API Manager 是一个开源的全生命周期 API 管理解决方案。它具有设计和文档化 API 并使用各种策略发布 API 的能力。还提供了一个开发者门户,应用程序开发人员可以在上面发现和订阅 API。它的安全组件为客户端应用程序提供了获取令牌的功能。我们可以在基于 Ballerina 的 API 网关中应用 API 策略。

在 API Manager 中为 API 生成 Ballerina 服务

Ballerina 是一门旨在让集成变得简单灵活的编程语言。它提供了集成领域所需要的所有构件,例如服务、端点、断路器等等。在为 WSO2 API Manager 创建 API 网关时,我们实际上是创建了与 API Manager 上发布的 API 相关的 Ballerina 服务。API Manager 定义了 API(资源路径、动词等)和每个 API 目标端点的详细信息,于是我们开发了一个工具,通过其 REST 接口连接到 API Manager,并基于一组模板将 API 定义转换为基于 Ballerina 的源代码。

同样的过程也适用于 API Manager 上定义的策略。所有策略(如 API Manager 上定义的配额策略)都将转换为 Ballerina 源代码。

下图描绘了代码的生成过程。

使用Ballerina构建API网关

生成的源代码被放到一个中,Ballerina 编译器编译它们,并生成一个可执行的二进制文件。

接下来,我们将介绍如何使用 Ballerina 的语言构建块和概念来创建 API 网关。我将解释使用每个构建块的原因,并通过代码演示如何使用它们。

Ballerina 作为一个简单的代理

API 网关上的 API 实际上是位于客户端应用程序和目标 API 之间的代理。它的核心职责是拦截来自客户端应用程序的请求,并确保对它们应用适当的策略。以下是 Ballerina 服务的一个简单示例。

复制代码
import ballerina/http;
import ballerina/log;
// 目标端点
endpoint http:Client targetEndpoint {
url: "https://api.pizzastore.com/pizzashack/v1"
};
// 可以通过 /pizzashack/1.0.0 和 9090 端口访问这个服务
@http:ServiceConfig {
basePath: "/pizzashack/1.0.0"
}
service<http:Service> passthrough bind { port: 9090 } {
@http:ResourceConfig {
methods:["GET"],
path: "/menu"
}
passthrough(endpoint caller, http:Request req) {
// 把客户端请求转发到目标端点的 /menu 上
var clientResponse = targetEndpoint->forward("/menu", req);
// 检查端点的响应是否成功
match clientResponse {
http:Response res => {
caller->respond(res) but { error e =>
log:printError("Error sending response", err = e) };
}
error err => {
http:Response res = new;
res.statusCode = 500;
res.setPayload(err.message);
caller->respond(res) but { error e =>
log:printError("Error sending response", err = e) };
}
}
}
}

上面是一个简单的 Ballerina 服务,它监听 9090 端口上的请求。这个服务的路径为 (basepath)/pizzashack/1.0.0,并定义了一个资源,用以接收子路径 /menu 上的 GET 请求。/menu 上的所有 GET 请求都会被转发到目标端点 https://api.pizzastore.com/pizzashack/v1/menu

请求过滤器

我们现在已经成功创建了一个简单的代理,下一步就是看看我们如何使网关对 API 请求强制执行各种 QoS,例如认证、授权、速率限定和分析。

你可能已经注意到,在上面的服务定义中,服务“绑定”了端口(9090)。实际上,服务绑定的是 Ballerina 在 9090 端口上的 HTTP 监听器。我们可以自定义 Ballerina 监听器,并把服务绑定到这些监听器上。每个监听器都可以拥有自己的一组请求过滤器。请求筛选器可以访问传入的请求数据,可用于检查请求的内容,对请求进行验证,复制并将请求推送到其他系统,甚至修改原始请求。我们选择实现一个自定义监听器,并将服务绑定到该监听器,这样就可以将自定义请求过滤器与监听器联系起来。以下是监听器的接口定义。

复制代码
public type APIGatewayListener object {
public {
EndpointConfiguration config;
http:Listener httpListener;
}
new () {
httpListener = new;
}
public function init(EndpointConfiguration config);
public function initEndpoint() returns (error);
public function register(typedesc serviceType);
public function start();
public function getCallerActions() returns (http:Connection);
public function stop();
};

监听器定义了一系列需要实现的函数,例如初始化、启动监听器、停止监听器等。现在不提供特定函数的实现,因为涉及了太多细节。

以下是认证请求过滤器的接口定义。filterRequest 函数中的代码对客户端应用程序进行身份验证,并将函数返回的 FilterResult 对象传给下一个过滤器。

复制代码
public type AuthnFilter object {
public function filterRequest (http:Request request, http:FilterContext
context) returns http:FilterResult {

现在我们已经知道如何声明我们自己的监听器和实现自己的请求过滤器,下一步要将我们的监听器声明为端点,并按照一定顺序将所有请求过滤器关联起来,如下所示。

复制代码
import wso2/gateway;
AuthnFilter authnFilter;
OAuthzFilter authzFilter;
RateLimitFilter rateLimitFilter;
AnalyticsFilter analyticsFilter;
ExtensionFilter extensionFilter;
endpoint gateway:APIGatewayListener apiListener {
port:9095,
filters:[authnFilter, authzFilter, rateLimitFilter,
analyticsFilter, extensionFilter]
};

我们已经导入了 wso2/gateway 包,因为我们的自定义 APIGatewayListener 就在这个包中。然后,我们声明了所有请求过滤器,它们将对传入的请求执行各种操作。最后,我们使用默认端口和所有过滤器声明了端点。

在声明了端点(监听器和过滤器)之后,最后一步就是将这个端点绑定到我们的代理服务。在上一节使用的示例服务中,我们将服务绑定到 9090 端口上的 HTTP 监听器。现在,我们有了一个功能齐全的端点,它可以针对 API 请求执行所有的 QoS。我们将服务定义更改如下。

复制代码
// 可以通过 /pizzashack/1.0.0 和 9095 端口访问这个服务
@http:ServiceConfig {
basePath: "/pizzashack/1.0.0"
}
service<http:Service> passthrough bind apiListener {

我们使用了 bind apiListener,而不是{port:9090}。这样我们的服务就可以监听我们声明的新端点,发到这个服务的所有请求都需要通过我们的请求过滤器,这些过滤器将执行所有相关操作。

通过 OAuth 来保护服务

认证

我们可以通过 OAuth 和基本身份验证来保护我们的服务。API 网关默认的保护模式是 OAuth,因为 OAuth 是保护 REST API 事实上的标准。你可以在 ballerina.io 网站上找到保护服务的示例。

首先,你需要在监听器定义中指定认证提供者,如下所示。

复制代码
http:AuthProvider jwtAuthProvider = {
scheme:"jwt",
issuer:"ballerina",
audience: "ballerina.io",
certificateAlias: "ballerina",
trustStore: {
path: "${ballerina.home}/bre/security/ballerinaTruststore.p12",
password: "ballerina"
}
};
endpoint gateway:APIGatewayListener apiListener {
port:9095,
filters: ....,
authProviders:[jwtAuthProvider]
};

也就是说,在监听器(9095 端口)上接收到的任何请求都应该通过 jwtAuthProvider,jwtAuthProvider 将验证传入的请求是否包含带有受信任 JWT 字符串(稍后将介绍更多 JWT 相关内容)的名为“Authorization”的 HTTP 头部字段。完成这些步骤后,接下来就是如何在 Ballerina 中保护服务。

复制代码
// 可以通过 /pizzashack/1.0.0 和 9090 端口访问这个服务
@http:ServiceConfig {
basePath: "/pizzashack/1.0.0",
authConfig: {
authentication: { enabled: true }
}
}

authConfig 实际上是可选的,因为服务绑定的监听器端点默认启用了安全检查。

授权

服务的每个操作都有相应的授权范围。传入请求需要附带令牌,并在相应的授权范围内访问资源或操作。以下是我们如何为资源或操作指定范围。

复制代码
@http:ResourceConfig {
methods:["GET"],
path: "/menu",
authConfig: {
scopes: ["list_menu"]
}
}
passthrough(endpoint caller, http:Request req) {

在上面的声明中,传入请求中的令牌需要带有一个名为“list_menu”的作用域,才能获得资源的访问权限。

默认情况下,客户端应用程序在调用网关上的安全服务时需要发送受网关信任的 STS(安全令牌服务)签名的令牌(JWT)。客户端请求需要符合 OAuth Bearer 配置。也就是说,令牌应该被包含在名为“Authorization”的 HTTP 头部字段中,它的值应该是“Bearer $ token”的形式。“信任”因子基于网关是否可以访问 STS 的公共证书。JWT 是一个字符串,使用”.“分隔为 3 部分,每个部分都必须是 base64 编码的。这 3 个部分分别是 JWT 的头部信息、JWT 消息体和 JWT 签名。以下是 JWT 的一个示例:

复制代码
{
"sub": "ballerina",
"iss": "ballerina",
"exp": 2818415019,
"iat": 1524575019,
"jti": "f5aded50585c46f2b8ca233d0c2a3c9d",
"aud": [
"ballerina",
"ballerina.org",
"ballerina.io"
],
"scope": "list_menu"
}

JWT 需要指定一些列范围,如果范围声明中包含了特定资源,就可以获得对这些资源的访问权限。

使用 Ballerina Streams 进行速率限定

在 API Manager 中,我们可以对 API 实施速率限定策略。策略是在 API Manager 的管理门户中定义的。速率限定策略基于单位时间内的请求数或数据带宽来定义请求配额。例如,策略“Silver”允许应用程序每分钟访问 API 2000 次。我们在 Ballerina 中使用 Streams 来实现此功能。我们可以构建流式查询,对从事件流上接收到的数据进行投影、过滤、窗口、流连接和模式操作。

服务成功处理每条消息后都会生成一个事件,这些事件被放入名为 requestStream 的流中,如下所示。RequestStreamDTO 对象中包含了策略所需的信息,其中有一些信息来自 HTTP 请求本身,有些信息则是由上游过滤器传过来的,例如认证过滤器。

复制代码
public stream<RequestStreamDTO> requestStream;
public function publishNonThrottleEvent(RequestStreamDTO request) {
requestStream.publish(request);
}

每个速率限定策略(Silver)都被包装成 Ballerina 函数,这些函数监听 requestStream。在流接收到事件后,我们查询事件中的数据,看看它是否满足策略中定义的条件。以下是这类函数的一个示例,它检查在 1 分钟时间窗口内应用程序发出的请求是否超过了 2000 个。

复制代码
function initSubscriptionSilverPolicy() {
stream<gateway:GlobalThrottleStreamDTO> resultStream;
stream<gateway:EligibilityStreamDTO> eligibilityStream;
forever {
from gateway:requestStream
select messageID, (subscriptionTier == "Silver") as isEligible,
subscriptionKey as throttleKey
=> (gateway:EligibilityStreamDTO[] counts) {
eligibilityStream.publish(counts);
}
from eligibilityStream
throttler:timeBatch(60000, 0)
where isEligible == true
select throttleKey, count(messageID) >= 2000 as isThrottled,
expiryTimeStamp
group by throttleKey
=> (gateway:GlobalThrottleStreamDTO[] counts) {
resultStream.publish(counts);
}

如果出现配额违规,就会创建一个事件并将其推送到 resultStream 中。RateLimitFilter 监听此 resultStream,并在出现配额违规时阻止请求。

用于聚合分析数据的 Ballerina File API

API Gateway 还可以将数据提供给 Analytics 引擎,生成业务洞察报告。成功的请求所生成的数据将定期以一定时间窗口为单位保存到文件中。网关可以托管多个服务,因此可能会有多个服务同时尝试将数据写入同一文件,从而可能导致文件出现长时间的写锁定。我们使用 Ballerina Streams 来避免这种情况。这样可以保证系统中只有一个线程将数据串行地写入文件,还可以确保文件写入是异步进行的。以下是 AnalyticsFilter 将事件写入流的代码。

复制代码
public function filterRequest(http:Request request, http:FilterContext context) returns http:FilterResult {
http:FilterResult requestFilterResult;
AnalyticsRequestStream requestStream = generateRequestEvent(request,
context);
EventDTO eventDto = generateEventFromRequest(requestStream);
eventStream.publish(eventDto);
requestFilterResult = { canProceed: true, statusCode: 200, message:
"Analytics filter processed." };
return requestFilterResult;
}

订阅 AnalyticsRequestStream 的函数使用 Ballerina Character I/O API 将数据写入文件。以下是执行此操作的代码。

复制代码
io:ByteChannel channel = io:openFile("api-usage-data.dat", io:APPEND);
io:CharacterChannel charChannel = new(channel, "UTF-8");
try {
io:println("writing to events to a file");
match charChannel.write(getEventData(eventDTO),0) {
int numberOfCharsWritten => {
io:println(" No of characters written : " +
numberOfCharsWritten);
}
error err => {
throw err;
}
}
} finally {
match charChannel.close() {
error sourceCloseError => {
io:println("Error occured while closing the channel: " +
sourceCloseError.message);
}
() => {
io:println("Source channel closed successfully.");
}
}
}

完成每个时间窗口后,当前文件被旋转(即重命名)并保存在文件系统中。然后,定期作业会将这些文件上载到 Analytics 引擎中,用于进一步分析处理。我们使用 Ballerina Task Timer 来实现此功能。Ballerina 支持 multipart 文件上传,因此我们可以将文件上传到 Analytics 引擎。

为 Docker 和 Kubernetes 生成可部署的工件

在微服务架构中,用户需要生成 API 网关的容器运行时。在 Ballerina 中,我们可以在服务上使用相关的 Docker 和 Kubernetes 注解,从而简化了这一过程。有关示例请参阅 Ballerina 的 GitHub 代码仓库。如上所述,我们有一个工具可以生成与 API Manager 上定义的 API 相对应的 Ballerina 源文件。如果网关的用户需要在构建阶段生成 Docker 镜像或 k8s 工件(我们将在下一节中讨论),那么在源代码生成阶段需要一组输入,并生成 Ballerina 源文件,其中包含了相关的 Docker 或 Kubernetes 注解。

让我们来看看与 Docker 相关的注解。你可以从 Ballerina GitHub 代码仓库的示例中找到有关 Kubernetes 注解的更多详细信息。为了将网关的监听端口映射到 Docker 容器宿主主的同一端口,需要对端点添加注解,如下所示。

复制代码
@docker:Expose{}
endpoint gateway:APIGatewayListener apiListener {
port:9095,
filters:[authnFilter, authzFilter, rateLimitFilter, analyticsFilter, extensionFilter]
};

然后,该服务需要添加包含 Docker 镜像和 Docker 注册表相关信息的注解。

复制代码
@docker:Config {
   registry:"private.docker.gateway.com",
   name:"passthrough",
   tag:"v1.0"
}
service<http:Service> passthrough bind apiListener {

在使用相关注解生成 Ballerina 源代码时,也会生成可以在容器上运行 Docker 镜像。

构建并运行网关

现在我们已经了解源代码是如何生成的,接下来让我们看看如何编译和运行网关。在生成的代码上运行标准的 Ballerina 构建命令(run)就可以生成可执行的工件。不过,前提是先在环境中安装 Ballerina。你有可能在开发环境生成代码,并在其他地方运行网关,因此我们构建了一个工具,该工具对 Ballerina 构建过程进行包装,将 Ballerina 运行时和 Ballerina 编译器生成的可执行文件嵌入到单个.zip 文件中。该文件的大小通常小于 50 兆(取决于你编译的服务数量),其中包含了 Ballerina 运行时(称为 BRE)、编译过的二进制文件和用于启动网关的 bash 脚本。如果是在标准的 VM 上进行部署,可以先解压这个文件,然后执行 bash 脚本。

如果生成的源代码中包含 @Docker 或 @Kubernetees 注解,则构建的输出文件就会不一样。对于 Docker 注解,构建命令会自动生成网关的 Docker 镜像,对于 Kubernetes 注解,它会生成 Kubernetes 工件,可以轻松地将它们部署到自动化环境中。

关于作者

使用Ballerina构建API网关Nuwan Dias 是开源集成提供商 WSO2 的总监。他是 WSO2 架构团队的成员,负责 WSO2 产品的战略和设计。Nuwan 专注于 API 和 API 管理。他大部分时间都在与 WSO2 的工程团队密切合作,该团队负责 WSO2 API Manager 的研发。Nuwan 是 Open API 安全组的成员,并在世界各地的众多会议上就 API、集成、安全和微服务等方面的主题发表演讲。

查看英文原文 Building an API Gateway with the Ballerina Programming Language

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论