BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

使用Ballerina构建API网关

| 作者 Nuwan Dias 关注 0 他的粉丝 ,译者 无明 关注 1 他的粉丝 发布于 2018年8月1日. 估计阅读时间: 33 分钟 | BCCon2018全球区块链生态技术大会,将区块链技术的创新和早期落地案例带回您的企业

关键要点

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

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

本文将介绍如何使用BallerinaWSO2 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编译器编译它们,并生成一个可执行的二进制文件。

接下来,我们将介绍如何使用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工件,可以轻松地将它们部署到自动化环境中。

关于作者

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账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT