Vert.x Web

Vert.x-Web是一组使用Vert.x构建Web应用程序的构建块。

把它看作是瑞士军刀,用于构建现代的,可扩展的网络应用程序。

Vert.x核心为处理HTTP提供了一个相当低级的功能集,并且对于某些应用程序来说足够了。

Vert.x-Web构建在Vert.x内核上,可以更轻松地为构建真实Web应用程序提供更丰富的功能。

它是Vert.x 2.x中Yoke的继承者,它Node.js世界中的ExpressRuby世界中的Sinatra等项目中获得灵感

Vert.x-Web被设计为功能强大,不受限制且完全可嵌入。你只需要使用你想要的零件,而没有更多。Vert.x-Web不是一个容器。

您可以使用Vert.x-Web创建经典的服务器端Web应用程序,RESTful Web应用程序,“实时”(服务器推送)Web应用程序或您可以想到的任何其他类型的Web应用程序。Vert.x-Web不关心。您需要选择您喜欢的应用类型,而不是Vert.x-Web。

Vert.x-Web非常适合编写RESTful HTTP微服务,但我们并不强迫您编写这样的应用程序。

Vert.x-Web的一些主要功能包括:

  • 路由(基于方法,路径等)

  • 路径的正则表达式模式匹配

  • 从路径中提取参数

  • 内容协商

  • 请求身体处理

  • 身体尺寸限制

  • Cookie解析和处理

  • 多部分表单

  • 多部分文件上传

  • 子路由器

  • 会话支持 - 本地(用于粘性会话)和群集(用于非粘性)

  • CORS(跨源资源共享)支持

  • 错误页处理程序

  • 基本认证

  • 基于重定向的认证

  • 授权处理程序

  • 基于JWT的授权

  • 用户/角色/权限授权

  • Favicon处理

  • 模板支持服务器端渲染,包括对以下模板引擎的支持:

    • 把手

    • 玉,

    • MVEL

    • Thymeleaf

    • Apache FreeMarker

    • 卵石

  • 响应时间处理器

  • 静态文件服务,包括缓存逻辑和目录列表。

  • 请求超时支持

  • SockJS支持

  • 事件总线桥

  • CSRF跨站请求伪造

  • 虚拟主机

Vert.x-Web中的大多数功能都是作为处理程序实现的,因此您可以随时编写自己的。我们设想随着时间的推移写更多。

我们将在本手册中讨论所有这些功能。

使用Vert.x Web

要使用vert.x web,请将以下依赖项添加到构建描述符依赖项部分:

  • Maven(在你的pom.xml):

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-web</artifactId>
  <version>3.5.1</version>
</dependency>
  • Gradle(在你的build.gradle文件中):

dependencies {
  compile 'io.vertx:vertx-web:3.5.1'
}

重新设置Vert.x核心HTTP服务器

Vert.x-Web使用并公开了Vert.x核心的API,所以如果你还没有熟悉使用Vert.x核心编写HTTP服务器的基本概念,那么非常值得。

Vert.x核心HTTP文档详细介绍了这一点。

这是一个使用Vert.x核心编写的hello world web服务器。此时没有涉及Vert.x-Web:

HttpServer server = vertx.createHttpServer();

server.requestHandler(request -> {

  // This handler gets called for each request that arrives on the server
  HttpServerResponse response = request.response();
  response.putHeader("content-type", "text/plain");

  // Write to the response and end it
  response.end("Hello World!");
});

server.listen(8080);

我们创建一个HTTP服务器实例,并在其上设置请求处理程序。每当请求到达服务器时,都会调用请求处理程序。

当发生这种情况时,我们只是将内容类型设置为text/plain,然后编写Hello World!并结束响应。

然后我们告诉服务器在端口上监听8080(默认主机是localhost)。

您可以运行此操作,并指向您的浏览器http://localhost:8080以验证它是否按预期工作。

基本的Vert.x-Web概念

这是10000英尺的视图:

A Router是Vert.x-Web的核心概念之一。这是一个维持零或更多的对象 Routes

路由器接受HTTP请求并找到该请求的第一条匹配路由,并将该请求传递给该路由。

该路由可以有一个处理程序与之关联,然后接收请求。然后可以对请求执行一些操作,然后结束它或将它传递给下一个匹配的处理程序。

这里有一个简单的路由器例子:

HttpServer server = vertx.createHttpServer();

Router router = Router.router(vertx);

router.route().handler(routingContext -> {

  // This handler will be called for every request
  HttpServerResponse response = routingContext.response();
  response.putHeader("content-type", "text/plain");

  // Write to the response and end it
  response.end("Hello World from Vert.x-Web!");
});

server.requestHandler(router::accept).listen(8080);

它基本上与上一节中的Vert.x Core HTTP服务器hello world示例相同,但是这次使用Vert.x-Web。

我们像以前一样创建一个HTTP服务器,然后我们创建一个路由器。一旦我们完成了这一步,我们就创建了一条没有匹配标准的简单路由,因此它将匹配到达服务器的所有请求。

然后,我们为该路线指定一个处理程序。该处理程序将被调用到达服务器上的所有请求。

传递给处理程序的对象是RoutingContext- 它包含标准的Vert.x HttpServerRequest以及HttpServerResponse 其他各种有用的东西,这些使Vert.x-Web更简单。

对于每个被路由的请求,都有一个唯一的路由上下文实例,并将同一个实例传递给该请求的所有处理程序。

一旦我们设置了处理程序,我们就设置HTTP服务器的请求处理程序来传递所有传入的请求accept

所以,这是基础知识。现在我们来看更详细的内容:

处理请求并调用下一个处理程序

当Vert.x-Web决定将请求路由到匹配路由时,它会调用路由传递的处理程序RoutingContext

如果你没有在你的处理程序中结束响应,你应该调用next另一个匹配的路由来处理请求(如果有的话)。

next处理程序完成执行之前,您不必调用如果你想要,你可以在一段时间后做这个:

Route route1 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  // enable chunked responses because we will be adding data as
  // we execute over other handlers. This is only required once and
  // only if several handlers do output.
  response.setChunked(true);

  response.write("route1\n");

  // Call the next matching route after a 5 second delay
  routingContext.vertx().setTimer(5000, tid -> routingContext.next());
});

Route route2 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  response.write("route2\n");

  // Call the next matching route after a 5 second delay
  routingContext.vertx().setTimer(5000, tid -> routingContext.next());
});

Route route3 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  response.write("route3");

  // Now end the response
  routingContext.response().end();
});

在上面的例子route1中写入响应,然后5秒钟后route2写入响应,然后5秒后route3写入响应,响应结束。

请注意,所有这些都不会发生任何线程阻塞。

使用阻塞处理程序

有时,您可能必须在处理程序中执行某些操作,以阻止事件循环一段时间,例如调用传统的阻塞API或进行一些密集计算。

你不能在普通的处理程序中这样做,所以我们提供了在路径上设置阻塞处理程序的功能。

阻塞处理程序看起来就像一个正常的处理程序,但它由Vert.x使用来自工作池的线程不使用事件循环来调用。

你用一个路由设置阻塞处理程序blockingHandler这是一个例子:

router.route().blockingHandler(routingContext -> {

  // Do something that might take some time synchronously
  service.doSomethingThatBlocks();

  // Now call the next handler
  routingContext.next();

});

默认情况下,在同一个上下文中执行的任何阻塞处理程序(例如相同的Verticle实例)都被排序 - 这意味着下一个处理程序只有在前一个完成时才会执行。如果你不关心orderering并且不介意你的阻塞处理器并行执行,你可以设置阻塞处理器指定ordered为false blockingHandler

请注意,如果您需要处理来自阻塞处理程序的多部分表单数据,则必须使用非阻塞处理程序FIRST才能进行调用setExpectMultipart(true)这里是一个例子:

router.post("/some/endpoint").handler(ctx -> {
  ctx.request().setExpectMultipart(true);
  ctx.next();
}).blockingHandler(ctx -> {
  // ... Do some blocking operation
});

按精确路径进行路由

可以设置路由以匹配来自请求URI的路径。在这种情况下,它将匹配具有与指定路径相同的路径的任何请求。

在下面的例子中,处理程序将被调用请求/some/path/我们也忽略了尾部的斜杠,所以它会被调用路径/some/path/some/path//也是:

Route route = router.route().path("/some/path/");

route.handler(routingContext -> {
  // This handler will be called for the following request paths:

  // `/some/path`
  // `/some/path/`
  // `/some/path//`
  //
  // but not:
  // `/some/path/subdir`
});

通过以某些内容开始的路径进行路由

通常,您希望路由以特定路径开头的所有请求。你可以使用一个正则表达式来做到这一点,但简单的方法是*在声明路径路径时在路径末尾使用星号

在下面的例子中,处理程序将被调用用于以URI开头的任何请求 /some/path/

例如/some/path/foo.html/some/path/otherdir/blah.css两者都会匹配。

Route route = router.route().path("/some/path/*");

route.handler(routingContext -> {
  // This handler will be called for any path that starts with
  // `/some/path/`, e.g.

  // `/some/path`
  // `/some/path/`
  // `/some/path/subdir`
  // `/some/path/subdir/blah.html`
  //
  // but not:
  // `/some/bath`
});

使用任何路径时,也可以在创建路由时指定:

Route route = router.route("/some/path/*");

route.handler(routingContext -> {
  // This handler will be called same as previous example
});

捕获路径参数

可以使用占位符来匹配路径,然后在请求中使用这些参数 params

这是一个例子

Route route = router.route(HttpMethod.POST, "/catalogue/products/:producttype/:productid/");

route.handler(routingContext -> {

  String productType = routingContext.request().getParam("producttype");
  String productID = routingContext.request().getParam("productid");

  // Do something with them...
});

占位符由:后跟参数名称组成。参数名称由任何字母字符,数字字符或下划线组成。

在上面的例子中,如果对路径发出POST请求,/catalogue/products/tools/drill123/那么路由将匹配并且productType将接收该值,tools并且productID将接收该值drill123

使用正则表达式路由

正则表达式也可以用来匹配路由中的URI路径。

Route route = router.route().pathRegex(".*foo");

route.handler(routingContext -> {

  // This handler will be called for:

  // /some/path/foo
  // /foo
  // /foo/bar/wibble/foo
  // /bar/foo

  // But not:
  // /bar/wibble
});

或者,可以在创建路由时指定正则表达式:

Route route = router.routeWithRegex(".*foo");

route.handler(routingContext -> {

  // This handler will be called same as previous example

});

使用正则表达式捕获路径参数

您还可以在使用正则表达式时捕获路径参数,以下是一个示例:

Route route = router.routeWithRegex(".*foo");

// This regular expression matches paths that start with something like:
// "/foo/bar" - where the "foo" is captured into param0 and the "bar" is captured into
// param1
route.pathRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(routingContext -> {

  String productType = routingContext.request().getParam("param0");
  String productID = routingContext.request().getParam("param1");

  // Do something with them...
});

在上面的例子中,如果请求路径:/tools/drill123/则路由将匹配并且productType将接收该值,tools并且productID将接收该值drill123

捕获在正则表达式中用捕获组表示(即用圆括号包围捕获)

使用命名的捕获组

在某些情况下使用int索引参数名称可能会很麻烦。可以在正则表达式路径中使用命名的捕获组。

  • [源,JAVA]

route route = router.routeWithRegex(“\\ /(?<productType> [^ \\ /] +)\\ /(?<productId> [^ \\ /] +)”)。handler(routingContext  - > {

  String productType = routingContext.request()。getParam(“productType”);
  String productID = routingContext.request()。getParam(“productId”);

  //和他们做些什么...
});

在上面的示例中,命名的捕获组映射到与组相同名称的路径参数。

此外,您仍然可以像使用普通组一样访问组参数(即params0, params1…​

通过HTTP方法进行路由

默认情况下,路由将匹配所有HTTP方法。

如果您希望路由只匹配您可以使用的特定HTTP方法 method

Route route = router.route().method(HttpMethod.POST);

route.handler(routingContext -> {

  // This handler will be called for any POST request

});

或者,您可以在创建路线时用路径指定:

Route route = router.route(HttpMethod.POST, "/some/path/");

route.handler(routingContext -> {

  // This handler will be called for any POST request to a URI path starting with /some/path/

});

如果你想航线特定HTTP方法,您还可以使用的方法,例如get postputHTTP方法的名字命名。例如:

router.get().handler(routingContext -> {

  // Will be called for any GET request

});

router.get("/some/path/").handler(routingContext -> {

  // Will be called for any GET request to a path
  // starting with /some/path

});

router.getWithRegex(".*foo").handler(routingContext -> {

  // Will be called for any GET request to a path
  // ending with `foo`

});

如果你想指定一个路由将会匹配多于HTTP方法,你可以method 多次调用

Route route = router.route().method(HttpMethod.POST).method(HttpMethod.PUT);

route.handler(routingContext -> {

  // This handler will be called for any POST or PUT request

});

路线顺序

默认情况下,路由按照它们添加到路由器的顺序进行匹配。

当请求到达时,路由器将遍历每条路由并检查它是否匹配,如果匹配,则将调用该路由的处理程序。

如果处理程序随后调用next下一个匹配路由的处理程序(如果有的话)将被调用。等等。

这里有一个例子来说明这一点:

Route route1 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  // enable chunked responses because we will be adding data as
  // we execute over other handlers. This is only required once and
  // only if several handlers do output.
  response.setChunked(true);

  response.write("route1\n");

  // Now call the next matching route
  routingContext.next();
});

Route route2 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  response.write("route2\n");

  // Now call the next matching route
  routingContext.next();
});

Route route3 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  response.write("route3");

  // Now end the response
  routingContext.response().end();
});

在上面的例子中,响应将包含:

ROUTE1
路径2
路径3

由于已按照该顺序调用路由,以便以任何请求开始/some/path

如果你想覆盖路由的默认排序,你可以使用order指定一个整数值。

路由在创建时被分配一个与它们被添加到路由器的顺序相对应的顺序,其中第一条路由编号0,第二条路由编号1等等。

通过指定路线的顺序,您可以覆盖默认顺序。订单也可以是负数,例如,如果您想确保路线在路线编号之前进行评估0

让我们更改route2的顺序,使其在route1之前运行:

Route route1 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  response.write("route1\n");

  // Now call the next matching route
  routingContext.next();
});

Route route2 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  // enable chunked responses because we will be adding data as
  // we execute over other handlers. This is only required once and
  // only if several handlers do output.
  response.setChunked(true);

  response.write("route2\n");

  // Now call the next matching route
  routingContext.next();
});

Route route3 = router.route("/some/path/").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  response.write("route3");

  // Now end the response
  routingContext.response().end();
});

// Change the order of route2 so it runs before route1
route2.order(-1);

那么响应现在将包含:

路径2
ROUTE1
路径3

如果两条匹配的路线具有相同的订单价值,那么它们将按照它们添加的顺序被调用。

您还可以指定最后处理路线 last

基于MIME请求类型的路由

您可以指定一个路由将匹配使用匹配的请求MIME类型consumes

在这种情况下,请求将包含content-type指定请求主体的MIME类型的头部。这将与在中指定的值匹配consumes

基本上,consumes描述处理程序可以使用哪些MIME类型

匹配可以在完全匹配的MIME类型上完成:

router.route().consumes("text/html").handler(routingContext -> {

  // This handler will be called for any request with
  // content-type header set to `text/html`

});

还可以指定多个完全匹配:

router.route().consumes("text/html").consumes("text/plain").handler(routingContext -> {

  // This handler will be called for any request with
  // content-type header set to `text/html` or `text/plain`.

});

支持在子类型上匹配通配符:

router.route().consumes("text/*").handler(routingContext -> {

  // This handler will be called for any request with top level type `text`
  // e.g. content-type header set to `text/html` or `text/plain` will both match

});

你也可以匹配顶级类型

router.route().consumes("*/json").handler(routingContext -> {

  // This handler will be called for any request with sub-type json
  // e.g. content-type header set to `text/json` or `application/json` will both match

});

如果你没有/在消费者中指定一个,它会假定你的意思是子类型。

基于客户端可接受的MIME类型进行路由

HTTP accept头用于表示响应的哪些MIME类型可以被客户端接受。

一个accept报头可具有由分隔的多个MIME类型“”。

MIME类型也可以q附加一个值*,表示如果有多个响应MIME类型与accept头匹配可用,则应用权重。q值是介于0和1.0之间的数字。如果省略,它默认为1.0。

例如,以下accept标题表示客户端将仅接受MIME类型text/plain

接受:text / plain

以下客户将接受text/plaintext/html不偏好。

接受:text / plain,text / html

以下客户会接受text/plaintext/html更喜欢,text/html因为它有更高的 q价值(默认值是q = 1.0)

接受:text / plain; q = 0.9,text / html

如果服务器可以提供文本/纯文本和文本/ html,则应在这种情况下提供文本/ html。

通过使用produces你定义的路由产生的MIME类型,例如下面的处理程序产生一个MIME类型的响应application/json

router.route().produces("application/json").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();
  response.putHeader("content-type", "application/json");
  response.write(someJSON).end();

});

在这种情况下,路线将与任何accept匹配标题的请求匹配application/json

以下是一些accept匹配标题示例

接受:application / json
接受:应用程序/ *
接受:application / json,text / html
接受:application / json; q = 0.7,text / html; q = 0.8,text / plain

您还可以将您的路线标记为生成多种MIME类型。如果是这种情况,那么您可以使用它getAcceptableContentType来找出被接受的实际MIME类型。

router.route().produces("application/json").produces("text/html").handler(routingContext -> {

  HttpServerResponse response = routingContext.response();

  // Get the actual MIME type acceptable
  String acceptableContentType = routingContext.getAcceptableContentType();

  response.putHeader("content-type", acceptableContentType);
  response.write(whatever).end();
});

在上面的例子中,如果你发送了一个带有以下accept头部的请求

接受:application / json; q = 0.7,text / html

然后,这条路线将会匹配并acceptableContentType包含,text/html因为两者都可以接受,但是具有更高的q价值。

结合路由标准

您可以用许多不同的方式组合所有上述路由标准,例如:

Route route = router.route(HttpMethod.PUT, "myapi/orders")
  .consumes("application/json")
  .produces("application/json");

route.handler(routingContext -> {

  // This would be match for any PUT method to paths starting with "myapi/orders" with a
  // content-type of "application/json"
  // and an accept header matching "application/json"

});

启用和禁用路由

您可以使用禁用路线disable匹配时,禁用的路线将被忽略。

您可以重新启用禁用的路线 enable

上下文数据

您可以使用上下文数据RoutingContext来维护您希望在请求的生命周期内处理程序之间共享的任何数据。

下面是一个例子,其中一个处理程序在上下文数据中设置了一些数据,随后的处理程序检索它:

您可以使用put放置任何对象,并 get从上下文数据中检索任何对象。

发送到路径的请求/some/path/other将匹配两个路由。

router.get("/some/path").handler(routingContext -> {

  routingContext.put("foo", "bar");
  routingContext.next();

});

router.get("/some/path/other").handler(routingContext -> {

  String bar = routingContext.get("foo");
  // Do something with bar
  routingContext.response().end();

});

或者,您可以使用访问整个上下文数据映射data

重新路由

到目前为止,所有的路由机制都允许您以顺序的方式处理您的请求,但是有时您可能想要返回。由于上下文不公开有关前一个或下一个处理程序的任何信息,主要是因为该信息是动态的,所以有一种方法可以从当前路由器的开始重新启动整个路由。

router.get("/some/path").handler(routingContext -> {

  routingContext.put("foo", "bar");
  routingContext.next();

});

router.get("/some/path/B").handler(routingContext -> routingContext.response().end());

router.get("/some/path").handler(routingContext -> routingContext.reroute("/some/path/B"));

因此,从代码中可以看到,如果请求/some/path首先向上下文中添加值,则会转到下一个处理程序,以重新路由/some/path/B终止请求的请求。

您可以基于新路径或基于新路径和方法重新路由。但请注意,基于方法的重新路由可能会导致安全问题,因为例如通常安全的GET请求可能会变成DELETE。

Reroute也被允许在失败处理程序上,但是由于re路由器的性质,当被称为当前状态码和失败原因被重置时。如果需要,重新路由的处理程序应该生成正确的状态码,例如:

router.get("/my-pretty-notfound-handler").handler(ctx -> ctx.response()
  .setStatusCode(404)
  .end("NOT FOUND fancy html here!!!"));

router.get().failureHandler(ctx -> {
  if (ctx.statusCode() == 404) {
    ctx.reroute("/my-pretty-notfound-handler");
  } else {
    ctx.next();
  }
});

应该清楚重新路由的工作paths,所以如果你需要在重新路由时保存或添加状态,应该使用该RoutingContext对象。例如,你想用一个额外的参数重新路由到一个新的路径:

router.get("/final-target").handler(ctx -> {
  // continue from here...
});

// THE WRONG WAY! (Will reroute to /final-target excluding the query string)
router.get().handler(ctx -> ctx.reroute("/final-target?variable=value"));

// THE CORRECT WAY!
router.get().handler(ctx -> ctx
  .put("variable", "value")
  .reroute("/final-target"));

即使错误的重新路由路径会警告您查询字符串被忽略,重新分配也会发生,因为实现会从路径中除去任何查询字符串或html片段。

子路由器

有时如果你有很多处理程序,把它们分成多个路由器是有意义的。如果要在不同的应用程序中重用一组处理程序,并以不同的路径根为根,这也很有用。

为此,您可以将路由器安装在另一个路由器安装点上。安装的路由器称为 子路由器子路由器可以安装其他子路由器,因此如果您愿意,您可以拥有多级子路由器。

我们来看一个安装在另一个路由器上的子路由器的简单例子。

这个子路由器将维护与简单的虚构REST API相对应的一组处理程序。我们将在另一台路由器上安装它。没有显示REST API的完整实现。

这是子路由器:

Router restAPI = Router.router(vertx);

restAPI.get("/products/:productID").handler(rc -> {

  // TODO Handle the lookup of the product....
  rc.response().write(productJSON);

});

restAPI.put("/products/:productID").handler(rc -> {

  // TODO Add a new product...
  rc.response().end();

});

restAPI.delete("/products/:productID").handler(rc -> {

  // TODO delete the product...
  rc.response().end();

});

如果此路由器用作顶级路由器,则GET / PUT / DELETE请求会像/products/product1234 调用API一样。

但是,假设我们已经有一个由另一个路由器描述的网站:

Router mainRouter = Router.router(vertx);

// Handle static resources
mainRouter.route("/static/*").handler(myStaticHandler);

mainRouter.route(".*\\.templ").handler(myTemplateHandler);

在这种情况下,我们现在可以将主路由器上的子路由器安装在安装点上 /productsAPI

mainRouter.mountSubRouter("/productsAPI", restAPI);

这意味着REST API现在可以通过如下路径访问: /productsAPI/products/product1234

本土化

Vert.x Web解析Accept-Language标题并提供一些帮助器方法来确定哪些是客户端的首选语言环境或按质量排序的首选语言环境的排序列表。

Route route = router.get("/localized").handler(rc -> {
  // although it might seem strange by running a loop with a switch we
  // make sure that the locale order of preference is preserved when
  // replying in the users language.
  for (LanguageHeader language : rc.acceptableLanguages()) {
    switch (language.tag()) {
      case "en":
        rc.response().end("Hello!");
        return;
      case "fr":
        rc.response().end("Bonjour!");
        return;
      case "pt":
        rc.response().end("Olá!");
        return;
      case "es":
        rc.response().end("Hola!");
        return;
    }
  }
  // we do not know the user language so lets just inform that back:
  rc.response().end("Sorry we don't speak: " + rc.preferredLocale());
});

主要方法acceptableLocales将返回用户可理解的语言环境的有序列表,如果您只对用户首选语言环境感兴趣,则帮助程序: preferredLocale将返回列表的第一个元素,或者null如果用户未提供区域设置。

默认404处理

如果没有路由匹配任何特定的请求,Vert.x-Web将发出404错误。

这可以通过您自己的错误处理程序或者我们提供的扩展错误处理程序来处理,或者如果没有提供错误处理程序,Vert.x-Web将返回一个基本的404(Not Found)响应。

错误处理

除了设置处理请求的处理程序外,您还可以设置处理程序来处理路由中的故障。

失败处理程序与完全相同的路径匹配标准一起使用,您可以使用正常的处理程序。

例如,您可以提供一个仅处理特定路径上的故障或某些HTTP方法的故障处理程序。

这使您可以为应用程序的不同部分设置不同的故障处理程序。

下面是一个失败处理程序的示例,只有在路由到以下路径的GET请求时才会出现故障/somepath/

Route route = router.get("/somepath/*");

route.failureHandler(frc -> {

  // This will be called for failures that occur
  // when routing requests to paths starting with
  // '/somepath/'

});

如果处理程序抛出异常,或者处理程序调用fail指定HTTP状态码故意发送故障信号,则会发生故障路由

如果从处理程序中捕获到异常,则会导致状态码发送失败500

处理失败时,失败处理程序将传递路由上下文,该上下文还允许检索失败或失败代码,以便失败处理程序可以使用它来生成失败响应。

Route route1 = router.get("/somepath/path1/");

route1.handler(routingContext -> {

  // Let's say this throws a RuntimeException
  throw new RuntimeException("something happened!");

});

Route route2 = router.get("/somepath/path2");

route2.handler(routingContext -> {

  // This one deliberately fails the request passing in the status code
  // E.g. 403 - Forbidden
  routingContext.fail(403);

});

// Define a failure handler
// This will get called for any failures in the above handlers
Route route3 = router.get("/somepath/*");

route3.failureHandler(failureRoutingContext -> {

  int statusCode = failureRoutingContext.statusCode();

  // Status code will be 500 for the RuntimeException or 403 for the other failure
  HttpServerResponse response = failureRoutingContext.response();
  response.setStatusCode(statusCode).end("Sorry! Not today");

});

为了在状态消息头中运行错误处理程序相关的不允许字符使用时发生错误,原始状态消息将从错误代码更改为默认消息。这是一个权衡,以保持HTTP协议的语义工作,而不是突然创建,并关闭套接字,但没有正确完成协议。

请求身体处理

BodyHandler允许您检索请求主体,限制车身尺寸和处理文件上传。

您应确保身体处理程序位于匹配路线上,以处理任何需要此功能的请求。

这个处理程序的用法要求它尽快安装在路由器中,因为它需要安装处理程序来使用HTTP请求主体,并且这必须在执行任何异步调用之前完成。

router.route().handler(BodyHandler.create());

获取请求主体

如果你知道请求主体是JSON,那么你可以使用getBodyAsJson,如果你知道它是一个你可以使用的字符串getBodyAsString,或者将它作为缓冲区使用检索getBody

限制身体尺寸

要限制请求主体的大小,请创建主体处理程序,然后使用setBodyLimit 指定最大主体大小(以字节为单位)。这对于避免耗尽内存非常庞大的机构很有用。

如果尝试发送大于最大尺寸的主体Request Entity Too Large,则会发送HTTP状态码413 - 

默认情况下不存在任何身体限制。

合并表单属性

默认情况下,主体处理程序会将任何表单属性合并到请求参数中。如果你不想要这种行为,你可以使用禁用它setMergeFormAttributes

处理文件上传

Body处理程序也用于处理多部分文件上传。

如果主体处理程序位于请求的匹配路径上,则任何文件上载都将自动流式传输到上传目录,这是file-uploads默认情况下的操作。

每个文件将被赋予一个自动生成的文件名,文件上传将在路由上下文中可用fileUploads

这是一个例子:

router.route().handler(BodyHandler.create());

router.post("/some/path/uploads").handler(routingContext -> {

  Set<FileUpload> uploads = routingContext.fileUploads();
  // Do something with uploads....

});

每个文件上传都由一个FileUpload实例来描述,它允许访问各种属性,如名称,文件名和大小。

处理cookies

Vert.x-Web支持使用CookieHandler

您应该确保cookie处理程序位于匹配路由上,以处理需要此功能的任何请求。

router.route().handler(CookieHandler.create());

操作饼干

您可以getCookie通过名称来检索cookie,或者用于cookies检索整个集合。

要删除cookie,请使用removeCookie

添加cookie使用addCookie

当响应标题被写入以便浏览器可以存储它们时,这组Cookie将被自动写回到响应中。

Cookie由实例描述Cookie这允许您检索名称,值,域,路径和其他正常的Cookie属性。

以下是查询和添加Cookie的示例:

router.route().handler(CookieHandler.create());

router.route("some/path/").handler(routingContext -> {

  Cookie someCookie = routingContext.getCookie("mycookie");
  String cookieValue = someCookie.getValue();

  // Do something with cookie...

  // Add a cookie - this will get written back in the response automatically
  routingContext.addCookie(Cookie.cookie("othercookie", "somevalue"));
});

处理会话

Vert.x-Web提供开箱即用的会话支持。

会话在HTTP请求之间持续浏览器会话的长度,并为您提供一个可添加会话范围信息(如购物篮)的位置。

Vert.x-Web使用会话cookie来识别会话。会话cookie是临时的,并且在关闭时将被浏览器删除。

我们不会将会话的实际数据放入会话cookie中 - 该cookie只是使用一个标识符来查找服务器上的实际会话。标识符是一个随机的UUID,使用安全的随机生成,所以它应该是有效的不可测的。

Cookie在HTTP请求和响应中传递,因此在使用会话时确保使用HTTPS总是明智的。如果您尝试通过直接HTTP使用会话,Vert.x会发出警告。

要在您的应用程序中启用会话,您必须SessionHandler 在应用程序逻辑之前在匹配路径上有一个

会话处理程序处理会话cookie的创建以及会话的查找,因此您不必亲自去做。

会话商店

要创建会话处理程序,您需要拥有一个会话存储实例。会话存储是保存应用程序实际会话的对象。

会话商店负责持有安全的伪随机数生成器,以保证安全的会话ID。这个PRNG独立于商店,这意味着给定一个来自商店的会话ID。由于商店具有不同的种子和状态,因此无法派生商店B的会话ID。

默认情况下,此PRNG使用混合模式,阻塞进行播种,非阻塞进行生成。PRNG还将每隔5分钟重新加入64位新熵。但是,这可以全部使用系统属性进行配置:

  • io.vertx.ext.auth.prng.algorithm例如:SHA1PRNG

  • io.vertx.ext.auth.prng.seed.interval例如:1000(每秒)

  • io.vertx.ext.auth.prng.seed.bits例如:128

大多数用户不需要配置这些值,除非您注意到应用程序的性能受到PRNG算法的影响。

Vert.x-Web提供两种会话存储实现,您也可以根据自己的喜好编写自己的会话存储实现。

本地会话存储

有了这个商店,会话本地存储在内存中,只有在这种情况下才可用。

如果您的应用程序中只有一个Vert.x实例正在使用粘性会话,并且已将负载平衡器配置为始终将HTTP请求路由到同一个Vert.x实例,则此存储是适当的。

如果您无法确保您的请求将全部在同一台服务器上终止,请不要使用此存储,因为您的请求可能会在不了解会话的服务器上结束。

本地会话存储通过使用共享本地地图来实现,并具有清除过期会话的收割者。

收割者间隔可以配置 LocalSessionStore.create

这里有一些创建a的例子 LocalSessionStore

SessionStore store1 = LocalSessionStore.create(vertx);

// Create a local session store specifying the local shared map name to use
// This might be useful if you have more than one application in the same
// Vert.x instance and want to use different maps for different applications
SessionStore store2 = LocalSessionStore.create(vertx, "myapp3.sessionmap");

// Create a local session store specifying the local shared map name to use and
// setting the reaper interval for expired sessions to 10 seconds
SessionStore store3 = LocalSessionStore.create(vertx, "myapp3.sessionmap", 10000);

集群会话存储

使用此商店,会话存储在可通过Vert.x群集访问的分布式映射中。

如果您使用粘性会话,则该商店是合适的,即您的负载均衡器将不同的请求从同一浏览器分发到不同的服务器。

您可以使用此存储从集群中的任何节点访问您的会话。

要使用群集会话存储,应确保Vert.x实例已群集。

这里有一些创建a的例子 ClusteredSessionStore

Vertx.clusteredVertx(new VertxOptions().setClustered(true), res -> {

  Vertx vertx = res.result();

  // Create a clustered session store using defaults
  SessionStore store1 = ClusteredSessionStore.create(vertx);

  // Create a clustered session store specifying the distributed map name to use
  // This might be useful if you have more than one application in the cluster
  // and want to use different maps for different applications
  SessionStore store2 = ClusteredSessionStore.create(vertx, "myclusteredapp3.sessionmap");
});

创建会话处理程序

创建会话存储后,您可以创建会话处理程序并将其添加到路由。你应该确保你的会话处理程序被路由到应用程序处理程序之前。

您还需要包含一个CookieHandler会话处理程序使用cookie来查找会话。路由时,Cookie处理程序应位于会话处理程序之前。

这是一个例子:

Router router = Router.router(vertx);

// We need a cookie handler first
router.route().handler(CookieHandler.create());

// Create a clustered session store using defaults
SessionStore store = ClusteredSessionStore.create(vertx);

SessionHandler sessionHandler = SessionHandler.create(store);

// Make sure all requests are routed through the session handler too
router.route().handler(sessionHandler);

// Now your application handlers
router.route("/somepath/blah/").handler(routingContext -> {

  Session session = routingContext.session();
  session.put("foo", "bar");
  // etc

});

会话处理器将确保您的会话自动从会话存储中查找(或在没有会话存在的情况下创建),并在路由上下文到达您的应用处理程序之前对其进行设置。

使用会话

在您的处理程序中,您可以使用访问会话实例session

您将数据放入会话中put,您从会话中获取数据get,并使用会话从会话中删除数据remove

会话中项目的键总是字符串。的值可以是任何类型的用于本地会话存储器,并用于一个集群会话存储器它们可以是任何基本类型,或者BufferJsonObject JsonArray或一个可序列化的对象,作为值必须在整个群集序列化。

以下是处理会话数据的示例:

router.route().handler(CookieHandler.create());
router.route().handler(sessionHandler);

// Now your application handlers
router.route("/somepath/blah").handler(routingContext -> {

  Session session = routingContext.session();

  // Put some data from the session
  session.put("foo", "bar");

  // Retrieve some data from a session
  int age = session.get("age");

  // Remove some data from a session
  JsonObject obj = session.remove("myobj");

});

在完成响应之后,会话会自动写回商店。

您可以使用手动销毁会话destroy这将从上下文和会话存储中删除会话。请注意,如果没有会话,将自动为通过会话处理程序路由的浏览器的下一个请求自动创建一个新会话。

会话超时

如果会话超过超时时间,会话将自动超时。会话超时后,会从商店中删除。

当请求到达时,会话将自动标记为已访问,并查看会话,并在响应完成时将会话存储回存储。

您也可以使用setAccessed手动将会话标记为已访问。

会话超时可以在创建会话处理程序时进行配置。默认超时时间为30分钟。

认证/授权

Vert.x附带一些用于处理认证和授权的开箱即用处理程序。

创建一个auth处理程序

要创建身份验证处理程序,您需要一个实例AuthProvider身份验证提供程序用于用户的身份验证和授权。Vert.x在vertx-auth项目中提供了几个auth提供程序实例。有关身份验证提供程序的完整信息以及如何使用和配置它们,请参阅身份验证文档。

给出一个auth提供者,这里有一个简单的例子来创建一个基本的auth处理程序。

router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));

AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider);

在您的应用程序中处理auth

假设您希望所有对以开头的路径的请求/private/受到授权。要做到这一点,请确保您的认证处理程序位于这些路径上的应用程序处理程序之前:

router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
router.route().handler(UserSessionHandler.create(authProvider));

AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider);

// All requests to paths starting with '/private/' will be protected
router.route("/private/*").handler(basicAuthHandler);

router.route("/someotherpath").handler(routingContext -> {

  // This will be public access - no login required

});

router.route("/private/somepath").handler(routingContext -> {

  // This will require a login

  // This will have the value true
  boolean isAuthenticated = routingContext.user() != null;

});

如果认证处理程序已成功认证并授权用户,它将向User 对象注入一个对象,RoutingContext以便在您的处理程序中使用: user

如果您希望将User对象存储在会话中,以便它们在请求之间可用,因此您无需对每个请求进行身份验证,那么您应该确保在auth之前在匹配路由上有会话处理程序和用户会话处理程序处理程序。

一旦你有你的用户对象,你也可以编程方式使用它的方法来授权用户。

如果你想让用户注销,你可以调用clearUser 路由上下文。

HTTP基本认证

HTTP基本身份验证是一种简单的身份验证方法,适用于简单的应用程序。

使用基本身份验证时,凭据将在HTTP标头中通过无线方式发送,因此,使用HTTPS而非HTTP为应用程序提供服务至关重要。

使用基本身份验证,如果用户请求需要授权的资源,基本身份验证处理程序将发回401带有标头WWW-Authenticate响应这会提示浏览器显示登录对话框并提示用户输入用户名和密码。

再次向资源发出请求,这次是使用Authorization标题集,包含在Base64中编码的用户名和密码。

当基本身份验证处理程序收到此信息时,会调用配置有用AuthProvider 户名和密码的用户身份验证。如果认证成功,则处理程序尝试授权用户。如果这是成功的,则允许请求的路由继续到应用处理程序,否则403返回响应以表示访问被拒绝。

身份验证处理程序可以使用访问要授予的资源所需的一组权限进行设置。

重定向认证处理程序

使用重定向身份验证处理时,如果用户尝试访问受保护资源且未登录,则该用户会被重定向到登录页面。

然后用户填写登录表单并提交。这是由对用户进行身份验证的服务器处理的,如果通过身份验证将用户重定向回原始资源。

要使用重定向身份验证,您需要配置一个实例,RedirectAuthHandler而不是基本的身份验证处理程序。

您还需要设置处理程序来为您的实际登录页面提供服务,并且需要一个处理程序来处理实际登录本身。为了处理登录,我们为此提供了一个预先构建的处理程序FormLoginHandler

以下是一个简单应用程序的示例,在默认重定向网址上使用重定向身份验证处理程序/loginpage

router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
router.route().handler(UserSessionHandler.create(authProvider));

AuthHandler redirectAuthHandler = RedirectAuthHandler.create(authProvider);

// All requests to paths starting with '/private/' will be protected
router.route("/private/*").handler(redirectAuthHandler);

// Handle the actual login
// One of your pages must POST form login data
router.post("/login").handler(FormLoginHandler.create(authProvider));

// Set a static server to serve static resources, e.g. the login page
router.route().handler(StaticHandler.create());

router.route("/someotherpath").handler(routingContext -> {
  // This will be public access - no login required
});

router.route("/private/somepath").handler(routingContext -> {

  // This will require a login

  // This will have the value true
  boolean isAuthenticated = routingContext.user() != null;

});

智威汤逊授权

使用JWT授权资源可以通过权限保护,没有足够权限的用户将被拒绝访问。

使用这个处理程序有两个步骤:

  • 设置一个处理程序发出令牌(或依赖第三方)

  • 设置处理程序以过滤请求

请注意,这2个处理程序应该只能在HTTPS上使用,而不能这样做,因为它允许嗅探运行中的令牌,从而导致会话劫持攻击。

以下是有关如何发布令牌的示例:

Router router = Router.router(vertx);

JsonObject authConfig = new JsonObject().put("keyStore", new JsonObject()
  .put("type", "jceks")
  .put("path", "keystore.jceks")
  .put("password", "secret"));

JWTAuth authProvider = JWTAuth.create(vertx, authConfig);

router.route("/login").handler(ctx -> {
  // this is an example, authentication should be done with another provider...
  if ("paulo".equals(ctx.request().getParam("username")) && "secret".equals(ctx.request().getParam("password"))) {
    ctx.response().end(authProvider.generateToken(new JsonObject().put("sub", "paulo"), new JWTOptions()));
  } else {
    ctx.fail(401);
  }
});

现在你的客户端有一个令牌,所有它需要的是,对于所有的后续请求,HTTP头 Authorization被填充:Bearer <token>例如:

Router router = Router.router(vertx);

JsonObject authConfig = new JsonObject().put("keyStore", new JsonObject()
  .put("type", "jceks")
  .put("path", "keystore.jceks")
  .put("password", "secret"));

JWTAuth authProvider = JWTAuth.create(vertx, authConfig);

router.route("/protected/*").handler(JWTAuthHandler.create(authProvider));

router.route("/protected/somepage").handler(ctx -> {
  // some handle code...
});

智威汤逊允许您将任何您喜欢的信息添加到令牌本身。通过这样做,服务器中不存在任何状态,它允许您扩展应用程序而不需要群集会话数据。为了向令牌添加数据,在创建令牌的过程中只需将数据添加到JsonObject参数中:

JsonObject authConfig = new JsonObject().put("keyStore", new JsonObject()
  .put("type", "jceks")
  .put("path", "keystore.jceks")
  .put("password", "secret"));

JWTAuth authProvider = JWTAuth.create(vertx, authConfig);

authProvider.generateToken(new JsonObject().put("sub", "paulo").put("someKey", "some value"), new JWTOptions());

消费时也一样:

Handler<RoutingContext> handler = rc -> {
  String theSubject = rc.user().principal().getString("sub");
  String someKey = rc.user().principal().getString("someKey");
};

配置所需的权限

使用任何身份验证处理程序,您还可以配置所需的权限来访问资源。

默认情况下,如果没有配置权限,则只需登录即可访问资源,否则用户必须登录(已通过身份验证)并拥有所需的权限。

以下是配置应用程序的示例,以便应用程序的不同部分需要不同的权限。请注意,权限的含义取决于您使用的底层auth提供程序。例如,有些可能支持基于角色/权限的模型,但其他人可能会使用其他模型。

AuthHandler listProductsAuthHandler = RedirectAuthHandler.create(authProvider);
listProductsAuthHandler.addAuthority("list_products");

// Need "list_products" authority to list products
router.route("/listproducts/*").handler(listProductsAuthHandler);

AuthHandler settingsAuthHandler = RedirectAuthHandler.create(authProvider);
settingsAuthHandler.addAuthority("role:admin");

// Only "admin" has access to /private/settings
router.route("/private/settings/*").handler(settingsAuthHandler);

链接多个auth处理程序

有时您想要在单个应用程序中支持多个authN / authZ机制。为此,您可以使用ChainAuthHandler链式身份验证处理程序将尝试在一系列处理程序上执行身份验证。该链既适用于AuthN也适用于AuthZ,所以如果认证在链的给定处理程序中有效,那么将使用相同的处理程序执行授权(如果请求)。

知道某些处理程序需要特定的提供程序非常重要,例如:

所以不希望提供商将在所有处理程序中共享。有些情况下可以在处理程序中共享提供程序,例如:

所以说,你要创建一个接受两个应用程序HTTP Basic AuthenticationForm Redirect你会开始配置你的链:

ChainAuthHandler chain = ChainAuthHandler.create();

// add http basic auth handler to the chain
chain.append(BasicAuthHandler.create(provider));
// add form redirect auth handler to the chain
chain.append(RedirectAuthHandler.create(provider));

// secure your route
router.route("/secure/resource").handler(chain);
// your app
router.route("/secure/resource").handler(ctx -> {
  // do something...
});

因此,当用户发出没有Authorization标头的请求时,这意味着链将无法通过基本身份验证处理程序进行身份验证,并尝试使用重定向处理程序进行身份验证。由于重定向处理程序总是重定向,因此您将被发送到您在该处理程序中配置的登录表单。

与vertx-web中的正常路由一样,auth chaning是一个序列,因此如果您希望回退到您的浏览器,并使用HTTP基本身份验证请求用户凭据而不是重定向,则需要的所有操作都是反向追加到连锁,链条。

现在假设您在提供Authorization的标题处发出请求Basic [token]在这种情况下,基本身份验证处理程序将尝试进行身份验证,如果成功,则链将停止并且vertx-web将继续处理您的处理程序。如果令牌无效,例如错误的用户名/密码,则该链接将继续到以下条目。在这个特定情况下,重定向认证处理程序。

提供静态资源

Vert.x-Web附带一个开箱即用的处理程序,用于提供静态Web资源,以便您可以非常轻松地编写静态Web服务器。

服务静态资源,如.html.css.js或任何其他静态资源,您使用的一个实例 StaticHandler

对由静态处理程序处理的路径的任何请求都将导致文件系统上的目录或类路径中的文件被提供。默认的静态文件目录是webroot可以配置的。

在以下示例中,所有对以下路径开始的请求/static/都将从目录中获取webroot

router.route("/static/*").handler(StaticHandler.create());

例如,如果存在具有路径/static/css/mystyles.css请求,静态服务将在目录中查找文件webroot/css/mystyle.css

它还会在类路径中寻找一个名为的文件webroot/css/mystyle.css这意味着你可以将所有的静态资源打包成一个jar文件(或者fatjar)并且像这样分发它们。

当Vert.x首次在类路径上找到资源时,它将其提取并将其缓存在磁盘上的临时目录中,因此每次都不需要执行此操作。

处理程序将处理范围感知请求。当客户端向静态资源发出请求时,处理程序会通过在Accept-Ranges头上声明单元来通知它可以处理范围感知请求然后包含Range具有正确单元和开始和结束索引标题的其他请求将接收到具有正确Content-Range标题的部分响应

配置缓存

默认情况下,静态处理程序将设置缓存头以使浏览器有效地缓存文件。

Vert.x的Web设置标题cache-controllast-modifieddate

cache-controlmax-age=86400默认设置为这相当于一天。这可以根据setMaxAgeSeconds需要进行配置

如果浏览器发送带有if-modified-since标题的GET或HEAD请求,并且该资源自该日期以来未被修改,304则返回状态以告知浏览器使用其本地缓存资源。

如果不需要处理缓存标头,则可以使用禁用setCachingEnabled

当启用缓存处理时,Vert.x-Web将缓存资源的最后修改日期,这样可以避免每次检查实际上次修改日期时的磁盘命中。

高速缓存中的条目具有到期时间,在此之后,磁盘上的文件将被再次检查并更新高速缓存条目。

如果你知道你的文件永远不会在磁盘上发生变化,那么缓存条目将永远不会过期。这是默认设置。

如果您知道服务器运行时您的文件可能会在磁盘上发生更改,那么您可以将文件只读设置为falsesetFilesReadOnly

要在任何时候启用可以在内存中缓存的最大条目数量,请使用 setMaxCacheSize

配置您可以使用的缓存条目的到期时间setCacheEntryTimeout

配置索引页

对根路径的任何请求/都将导致索引页面被提供。默认情况下,索引页是index.html这可以使用配置setIndexPage

更改Web根目录

默认情况下,静态资源将从目录中提供webroot配置此用途 setWebRoot

提供隐藏的文件

默认情况下,服务器将提供隐藏文件(以文件开头.)。

如果你不想隐藏的文件被提供,你可以配置它setIncludeHidden

目录列表

服务器也可以执行目录列表。默认情况下,目录列表被禁用。启用它 setDirectoryListing

当启用目录列表时,返回的内容取决于accept标题中的内容类型

对于text/html目录列表,可以使用配置用于呈现目录列表页面的模板 setDirectoryTemplate

禁用磁盘上的文件缓存

默认情况下,Vert.x会将从类路径提供的文件缓存到磁盘上的文件.vertx中当前工作目录中调用的目录的子目录中。这在将服务部署为生产环境中的fatjar时非常有用,因为每次从类路径提供文件的速度可能会很慢。

在开发过程中,这可能会导致问题,就像在服务器运行时更​​新静态内容一样,缓存的文件将不会被更新的文件提供。

要禁用文件缓存可以提供您vert.x选项的属性fileResolverCachingEnabledfalse为了向后兼容,它也将该值默认为系统属性vertx.disableFileCaching例如,您可以在您的IDE中设置运行配置,以便在运行主类时进行设置。

CORS处理

跨源资源共享是允许从一个域请求资源并从另一个域进行服务的安全机制。

Vert.x-Web包含一个CorsHandler为您处理CORS协议的处理程序

这是一个例子:

router.route().handler(CorsHandler.create("vertx\\.io").allowedMethod(HttpMethod.GET));

router.route().handler(routingContext -> {

  // Your app handlers

});

模板

Vert.x-Web包括动态页面生成功能,包括对几种常用模板引擎的开箱即用支持。您也可以轻松添加自己的。

模板引擎被描述TemplateEngine为了渲染一个模板 render被使用。

使用模板最简单的方法不是直接调用模板引擎,而是使用 TemplateHandler该处理程序根据HTTP请求中的路径为您调用模板引擎。

默认情况下,模板处理程序将在名为的目录中查找模板templates这可以配置。

该处理程序将返回text/html默认内容类型的渲染结果这也可以配置。

当您创建模板处理程序时,您将传入您想要的模板引擎的实例。模板引擎没有嵌入到vertex-web中,因此,您需要配置项目以访问它们。为每个模板引擎提供配置。

这里有些例子:

TemplateEngine engine = HandlebarsTemplateEngine.create();
TemplateHandler handler = TemplateHandler.create(engine);

// This will route all GET requests starting with /dynamic/ to the template handler
// E.g. /dynamic/graph.hbs will look for a template in /templates/graph.hbs
router.get("/dynamic/*").handler(handler);

// Route all GET requests for resource ending in .hbs to the template handler
router.getWithRegex(".+\\.hbs").handler(handler);

MVEL模板引擎

要使用MVEL,您需要将以下依赖项添加到您的项目中: io.vertx:vertx-web-templ-mvel:3.5.1使用以下命令创建MVEL模板引擎的实例: io.vertx.ext.web.templ.MVELTemplateEngine#create()

当使用MVEL模板引擎时,.templ如果文件名中没有指定扩展名,它将默认查找带有扩展名的模板

RoutingContextMVEL模板中可以使用路由上下文作为context变量,这意味着您可以根据上下文中的任何内容(包括请求,响应,会话或上下文数据)呈现模板。

这里有些例子:

请求路径是@ {context.request()。path()}

会话中的变量'foo'是@ {context.session()。get('foo')}

上下文数据中的'bar'值是@ {context.get('bar')}

请参考MVEL模板文档以了解如何编写MVEL模板。

玉器模板引擎

要使用Jade模板引擎,需要将以下依赖项添加到项目中: io.vertx:vertx-web-templ-jade:3.5.1使用:创建一个Jade模板引擎的实例 io.vertx.ext.web.templ.JadeTemplateEngine#create()

在使用Jade模板引擎时,.jade如果文件名中没有指定扩展名,它将默认查找带有扩展名的模板

RoutingContextJade模板中的路由上下文可用作context变量,这意味着您可以根据上下文中的任何内容(包括请求,响应,会话或上下文数据)呈现模板。

这里有些例子:


HTML
    title = context.get('foo')+ context.request()。path()
  身体

请参阅Jade4j文档以了解如何编写Jade模板。

把手模板引擎

要使用Handlebars,您需要将以下依赖项添加到您的项目中: io.vertx:vertx-web-templ-handlebars:3.5.1使用:创建一个Handlebars模板引擎的实例io.vertx.ext.web.templ.HandlebarsTemplateEngine#create()

在使用Handlebars模板引擎时,.hbs如果文件名中没有指定扩展名,它将默认查找带有扩展名的模板

Handlebars模板无法在对象中调用任意方法,所以我们不能只将路由上下文传递到模板中,并让模板像我们可以用其他模板引擎一样反省它。

相反,该上下文data在模板中可用。

如果要访问其他数据(如请求路径),请求参数或会话数据,则应在模板处理程序之前将处理器中的上下文数据添加到处理程序中。例如:

TemplateHandler handler = TemplateHandler.create(engine);

router.get("/dynamic").handler(routingContext -> {

  routingContext.put("request_path", routingContext.request().path());
  routingContext.put("session_data", routingContext.session().data());

  routingContext.next();
});

router.get("/dynamic/").handler(handler);

请参阅Handlebars Java端口文档以了解如何编写句柄模板。

Thymeleaf模板引擎

要使用Thymeleaf,您需要将以下依赖项添加到您的项目中: io.vertx:vertx-web-templ-thymeleaf:3.5.1使用:创建Thymeleaf模板引擎的一个实例io.vertx.ext.web.templ.ThymeleafTemplateEngine#create()

在使用Thymeleaf模板引擎时,.html如果文件名中没有指定扩展名,它将默认查找带扩展名的模板

路由上下文RoutingContext在Thymeleaf模板中可用作context变量,这意味着您可以根据上下文中的任何内容(包括请求,响应,会话或上下文数据)呈现模板。

这里有些例子:

[剪断]
<p th:text =“$ {context.get('foo')}”> </ p>
<p th:text =“$ {context.get('bar')}”> </ p>
<p th:text =“$ {context.normalisedPath()}”> </ p>
<p th:text =“$ {context.request()。params()。get('param1')}”> </ p>
<p th:text =“$ {context.request()。params()。get('param2')}”> </ p>
[剪断]

请参阅Thymeleaf文档以了解如何编写Thymeleaf模板。

Apache FreeMarker模板引擎

要使用Apache FreeMarker,您需要将以下依赖项添加到您的项目中: io.vertx:vertx-web-templ-freemarker:3.5.1使用:创建Apache FreeMarker模板引擎的一个实例io.vertx.ext.web.templ.FreeMarkerTemplateEngine#create()

在使用Apache FreeMarker模板引擎时,.ftl如果文件名中未指定扩展名,它将默认查找带扩展名的模板

路由上下文RoutingContext在Apache FreeMarker模板中作为context变量提供,这意味着您可以根据上下文中的任何内容(包括请求,响应,会话或上下文数据)呈现模板。

这里有些例子:

[剪断]
<p th:text =“$ {context.foo}”> </ p>
<p th:text =“$ {context.bar}”> </ p>
<p th:text =“$ {context.normalisedPath()}”> </ p>
<p th:text =“$ {context.request()。params()。param1}”> </ p>
<p th:text =“$ {context.request()。params()。param2}”> </ p>
[剪断]

有关如何编写Apache FreeMarker模板的信息请参阅Apache FreeMarker文档

卵石模板引擎

要使用Pebble,您需要将以下依赖项添加到您的项目中: io.vertx:vertx-web-templ-pebble:3.5.1使用:创建一个Pebble模板引擎的实例io.vertx.ext.web.templ.PebbleTemplateEngine#create(vertx)

在使用Pebble模板引擎时,.peb如果文件名中没有指定扩展名,它将默认查找带扩展名的模板

路径上下文RoutingContext在Pebble模板中作为context变量提供,这意味着您可以根据上下文中的任何内容(包括请求,响应,会话或上下文数据)呈现模板。

这里有些例子:

[剪断]
<p th:text =“{{context.foo}}”> </ p>
<p th:text =“{{context.bar}}”> </ p>
<p th:text =“{{context.normalisedPath()}}”> </ p>
<p th:text =“{{context.request()。params()。param1}}”> </ p>
<p th:text =“{{context.request()。params()。param2}}”> </ p>
[剪断]

请查阅Pebble文档以了解如何编写Pebble模板。

禁用缓存

在开发过程中,您可能希望禁用模板缓存,以便对每个请求重新评估模板。为了做到这一点,你需要设置系统属性:io.vertx.ext.web.TemplateEngine.disableCacheto true

默认情况下它将是错误的。所以缓存总是启用。

错误处理程序

您可以使用模板处理程序或其他方式渲染自己的错误,但Vert.x-Web还包含一个非常漂亮的错误处理程序,它可以为您提供错误页面。

处理程序是ErrorHandler要使用错误处理程序,只需将其设置为您想要覆盖的任何路径的失败处理程序。

请求记录器

Vert.x-Web包含一个LoggerHandler可用于记录HTTP请求的处理程序

默认情况下,请求会记录到可以配置为使用JUL日志记录,log4j或SLF4J的Vert.x记录器。

LoggerFormat

服务图标

Vert.x-Web包含处理器,FaviconHandler特别是用于提供favicon。

可以使用指向文件系统的路径指定Favicons,或者默认情况下,Vert.x-Web将使用名称在类路径中查找文件favicon.ico这意味着您将favicon封装在应用程序的jar包中。

超时处理程序

Vert.x-Web包含一个超时处理程序,如果处理时间过长,您可以使用它来超时请求。

这是使用一个实例配置的TimeoutHandler

如果在写入503响应之前请求超时,则会将响应返回给客户端。

下面是一个使用超时处理程序的例子,它将/foo超过5秒后开始的所有路径请求超时

router.route("/foo/").handler(TimeoutHandler.create(5000));

响应时间处理器

该处理程序设置x-response-time包含从接收到请求到写入响应标题的时间(以ms为单位)的标头响应标头,例如:

x-response-time:1456ms

内容类型处理器

ResponseContentTypeHandler可以设置Content-Type自动报头。假设我们正在构建一个RESTful Web应用程序。我们需要在我们所有的处理程序中设置内容类型:

router.get("/api/books").produces("application/json").handler(rc -> findBooks(ar -> {
  if (ar.succeeded()) {
    rc.response().putHeader("Content-Type", "application/json").end(toJson(ar.result()));
  } else {
    rc.fail(ar.cause());
  }
}));

如果API表面变得相当大,则设置内容类型会变得麻烦。为了避免这种情况,请将ResponseContentTypeHandler相应的路由添加到:

router.route("/api/*").handler(ResponseContentTypeHandler.create());
router.get("/api/books").produces("application/json").handler(rc -> findBooks(ar -> {
  if (ar.succeeded()) {
    rc.response().end(toJson(ar.result()));
  } else {
    rc.fail(ar.cause());
  }
}));

处理程序从中获取适当的内容类型getAcceptableContentType因此,您可以轻松共享相同的处理程序来生成不同类型的数据:

router.route("/api/*").handler(ResponseContentTypeHandler.create());
router.get("/api/books").produces("text/xml").produces("application/json").handler(rc -> findBooks(ar -> {
  if (ar.succeeded()) {
    if (rc.getAcceptableContentType().equals("text/xml")) {
      rc.response().end(toXML(ar.result()));
    } else {
      rc.response().end(toJson(ar.result()));
    }
  } else {
    rc.fail(ar.cause());
  }
}));

SockJS

SockJS是一个客户端JavaScript库和协议,它提供了一个简单的类似WebSocket的接口,允许您连接到SockJS服务器,而不管实际的浏览器或网络是否允许真正的WebSocket。

它通过支持浏览器和服务器之间的各种不同传输,并根据浏览器和网络功能在运行时选择一个传输。

所有这些对你来说都是透明的 - 你只需要提供类似WebSocket的界面即可

请参阅SockJS网站以获取有关SockJS的更多信息。

SockJS处理程序

Vert.x提供了一个SockJSHandler在Vert.x-Web应用程序中使用SockJS 的开箱即处理函数

您应该使用每个SockJS应用程序创建一个处理程序SockJSHandler.create您还可以在创建实例时指定配置选项。配置选项用一个实例描述SockJSHandlerOptions

Router router = Router.router(vertx);

SockJSHandlerOptions options = new SockJSHandlerOptions().setHeartbeatInterval(2000);

SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options);

router.route("/myapp/*").handler(sockJSHandler);

处理SockJS插座

在服务器端,您在SockJS处理程序上设置一个处理程序,并且每次从客户端进行SockJS连接时都会调用它:

传递给处理程序的对象是a SockJSSocket这有一个熟悉的类似于套接字的接口,您可以像a NetSocket或a 一样读取和写入WebSocket它还实现了ReadStream WriteStream因此您可以将它从其他读取和写入流中抽出。

下面是一个简单的SockJS处理程序的例子,它简单地回显任何它读取的数据:

Router router = Router.router(vertx);

SockJSHandlerOptions options = new SockJSHandlerOptions().setHeartbeatInterval(2000);

SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options);

sockJSHandler.socketHandler(sockJSSocket -> {

  // Just echo the data back
  sockJSSocket.handler(sockJSSocket::write);
});

router.route("/myapp/*").handler(sockJSHandler);

客户端

在客户端JavaScript中,您使用SockJS客户端库进行连接。

你可以在这里找到

使用SockJS JavaScript客户端的完整详细信息在SockJS网站上,但是总的来说,您可以使用如下所示的内容:

var sock = new SockJS('http://mydomain.com/myapp');

sock.onopen = function(){
  的console.log( '开放');
};

sock.onmessage =函数(e){
  console.log('message',e.data);
};

sock.onclose = function(){
  的console.log( '关闭');
};

sock.send( '试验');

sock.close();

配置SockJS处理程序

处理程序可以使用各种选项进行配置SockJSHandlerOptions

insertJSESSIONID

插入一个JSESSIONID cookie,以便负载均衡器确保对特定SockJS会话的请求始终路由到正确的服务器。默认是true

sessionTimeout

服务器发送一个close事件,当一个客户端接收连接一段时间没有看到。该延迟由此设置配置。默认情况下,close当接收连接在5秒内未被看到时,将发出事件。

heartbeatInterval

为了防止代理和负载均衡器关闭长时间运行的http请求,我们需要假装连接处于活动状态并偶尔发送一个心跳包。这个设置控制了这个过程的频率。默认情况下,心跳数据包每25秒发送一次。

maxBytesStreaming

大多数流传输在客户端保存响应,并且不释放传递消息使用的内存。这种运输工具需要一段时间进行垃圾收集。max_bytes_streaming设置可以通过单个http流请求发送的最小字节数,然后它将被关闭。之后,客户端需要打开新的请求。将此值设置为1将有效地禁用流式传输,并使流式传输的行为与轮询传输类似。默认值是128K。

libraryURL

本身不支持跨域通信的传输('eventsource'命名为)使用iframe技巧。一个简单的页面由SockJS服务器提供(使用其外部域)并放置在一个不可见的iframe中。从此iframe运行的代码无需担心跨域问题,因为它是从域本地运行到SockJS服务器。此iframe也需要加载SockJS javascript客户端库,并且此选项可让您指定其url(如果您不确定,请将其指向最新缩小的SockJS客户端发行版,这是默认值)。默认值是http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js

disabledTransports

这是您要禁用的传输列表。可能的值是WEBSOCKET,EVENT_SOURCE,HTML_FILE,JSON_P,XHR。

SockJS事件总线桥

Vert.x-Web带有一个内置的SockJS套接字处理程序,称为事件总线桥,它有效地将服务器端Vert.x事件总线扩展为客户端JavaScript。

这创建了一个分布式事件总线,它不仅跨越服务器端的多个Vert.x实例,还包括在浏览器中运行的客户端JavaScript。

因此,我们可以创建一个包含许多浏览器和服务器的大型分布式总线。只要服务器已连接,浏览器就不必连接到同一台服务器。

这是通过提供一个简单的客户端JavaScript库来完成的,该库vertx-eventbus.js提供了一个非常类似于服务器端Vert.x事件总线API的API,它允许您将消息发送和发布到事件总线并注册处理程序以接收消息。

这个JavaScript库使用JavaScript SockJS客户端通过终止于SockJSHandler服务器端的SockJS连接来传输事件总线流量

然后安装一个特殊的SockJS套接字处理程序,SockJSHandler该处理程序处理SockJS数据并将其与服务器端事件总线进行桥接。

要激活网桥,只需调用 bridgeSockJS处理程序。

Router router = Router.router(vertx);

SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
BridgeOptions options = new BridgeOptions();
sockJSHandler.bridge(options);

router.route("/eventbus/*").handler(sockJSHandler);

在客户端JavaScript中,您可以使用'vertx-eventbus.js`库创建到事件总线的连接并发送和接收消息:

<script src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
<script src='vertx-eventbus.js'></script>

<script>

var eb = new EventBus('http://localhost:8080/eventbus');

eb.onopen = function() {

  // set a handler to receive a message
  eb.registerHandler('some-address', function(error, message) {
    console.log('received a message: ' + JSON.stringify(message));
  });

  // send a message
  eb.send('some-address', {name: 'tim', age: 587});

}

</script>

该示例所做的第一件事是创建事件总线的实例

var eb = new EventBus('http://localhost:8080/eventbus');

构造函数的参数是连接到事件总线的URI。既然我们用前缀创建了我们的桥,eventbus我们将连接到那里。

在连接打开之前,您实际上无法做任何事情。当它打开时,onopen处理程序将被调用。

该桥支持自动重新连接,具有可配置的延迟和退避选项。

var eb = new EventBus('http://localhost:8080/eventbus');
eb.enableReconnect(true);
eb.onopen = function() {}; // Set up handlers here, will be called on initial connection and all reconnections
eb.onreconnect = function() {}; // Optional, will only be called on reconnections

// Alternatively, pass in an options object
var options = {
    vertxbus_reconnect_attempts_max: Infinity, // Max reconnect attempts
    vertxbus_reconnect_delay_min: 1000, // Initial delay (in ms) before first reconnect attempt
    vertxbus_reconnect_delay_max: 5000, // Max delay (in ms) between reconnect attempts
    vertxbus_reconnect_exponent: 2, // Exponential backoff factor
    vertxbus_randomization_factor: 0.5 // Randomization factor between 0 and 1
};

var eb2 = new EventBus('http://localhost:8080/eventbus', options);
eb2.enableReconnect(true);
// Set up handlers...

您可以使用依赖关系管理器检索客户端库:

  • Maven(在你的pom.xml):

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-web</artifactId>
  <version>3.5.1</version>
  <classifier>client</classifier>
  <type>js</type>
</dependency>
  • Gradle(在你的build.gradle文件中):

compile 'io.vertx:vertx-web:3.5.1:client'

该图书馆还可用于:

请注意,API在3.0.0和3.1.0版本之间已更改。请检查更新日志。以前的客户端仍然兼容,仍然可以使用,但新客户端提供更多功能,并且更接近vert.x事件总线API。

保护桥

如果你像上面的例子那样开始建造一座桥而不保护它,并试图通过它发送信息,你会发现信息神秘地消失了。他们发生了什么?

对于大多数应用程序,您可能不希望客户端JavaScript能够将任何消息发送到服务器端的任何处理程序或所有其他浏览器。

例如,您可能在事件总线上有一项服务,允许访问或删除数据。我们不希望恶意行为或恶意客户能够删除数据库中的所有数据!

另外,我们不一定希望任何客户端能够听取任何事件总线地址。

为了解决这个问题,SockJS桥默认会拒绝任何消息。您需要告诉桥接哪些信息可以通过。(答复消息总是允许通过的例外情况)。

换句话说,网桥就像一种具有默认拒绝所有策略的防火墙

配置网桥以告诉它应该通过哪些消息很容易。

您可以使用您在调用网桥时传入的内容指定要允许入站和出站流量 匹配的匹配项BridgeOptions

每场比赛都是一个PermittedOptions对象:

setAddress

这表示消息正被发送到的确切地址。如果您想允许基于确切地址的邮件使用此字段。

setAddressRegex

这是一个与地址匹配的正则表达式。如果你想允许基于正则表达式的消息,你可以使用这个字段。如果该address字段被指定,则该字段将被忽略。

setMatch

这允许您允许基于其结构的消息。消息中的任何字段都必须存在于具有相同值的消息中才能被允许。目前这只适用于JSON消息。

如果消息是入站(即从客户端的JavaScript被发送到服务器),当它收到Vert.x的Web看起来通过任何入境许可匹配。如果有任何匹配,它将被允许通过。

如果消息在发送到客户端之前出站(即从服务器发送到客户端JavaScript),Vert.x-Web将查看任何出站允许的匹配。如果有任何匹配,它将被允许通过。

实际匹配的工作如下:

如果address已经指定了一个字段,那么address必须与消息的地址完全匹配,以使其被认为是匹配的。

如果address未指定字段并指定了addressRegex字段,则正则表达式address_re必须与消息的地址匹配才能被视为匹配。

如果match指定了一个字段,那么消息的结构也必须匹配。通过查看匹配对象中的所有字段和值并检查它们全部存在于实际的消息体中来构造匹配的作品。

这是一个例子:

Router router = Router.router(vertx);

SockJSHandler sockJSHandler = SockJSHandler.create(vertx);


// Let through any messages sent to 'demo.orderMgr' from the client
PermittedOptions inboundPermitted1 = new PermittedOptions().setAddress("demo.orderMgr");

// Allow calls to the address 'demo.persistor' from the client as long as the messages
// have an action field with value 'find' and a collection field with value
// 'albums'
PermittedOptions inboundPermitted2 = new PermittedOptions().setAddress("demo.persistor")
  .setMatch(new JsonObject().put("action", "find")
    .put("collection", "albums"));

// Allow through any message with a field `wibble` with value `foo`.
PermittedOptions inboundPermitted3 = new PermittedOptions().setMatch(new JsonObject().put("wibble", "foo"));

// First let's define what we're going to allow from server -> client

// Let through any messages coming from address 'ticker.mystock'
PermittedOptions outboundPermitted1 = new PermittedOptions().setAddress("ticker.mystock");

// Let through any messages from addresses starting with "news." (e.g. news.europe, news.usa, etc)
PermittedOptions outboundPermitted2 = new PermittedOptions().setAddressRegex("news\\..+");

// Let's define what we're going to allow from client -> server
BridgeOptions options = new BridgeOptions().
  addInboundPermitted(inboundPermitted1).
  addInboundPermitted(inboundPermitted1).
  addInboundPermitted(inboundPermitted3).
  addOutboundPermitted(outboundPermitted1).
  addOutboundPermitted(outboundPermitted2);

sockJSHandler.bridge(options);

router.route("/eventbus/*").handler(sockJSHandler);

要求对消息进行授权

事件总线网桥也可以配置为使用Vert.x-Web授权功能来要求对桥上的入站或出站消息进行授权。

为此,您可以将额外的字段添加到上一节中描述的匹配项中,以确定匹配所需的权限。

要声明登录用户的特定权限是必需的,才能访问允许使用该setRequiredAuthority字段的邮件 

这是一个例子:

PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.orderService");

// But only if the user is logged in and has the authority "place_orders"
inboundPermitted.setRequiredAuthority("place_orders");

BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted);

要授权用户,他们必须先登录,然后获得所需的权限。

要处理登录并实际验证您可以配置正常的Vert.x验证处理程序。例如:

Router router = Router.router(vertx);

// Let through any messages sent to 'demo.orderService' from the client
PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.orderService");

// But only if the user is logged in and has the authority "place_orders"
inboundPermitted.setRequiredAuthority("place_orders");

SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
sockJSHandler.bridge(new BridgeOptions().
  addInboundPermitted(inboundPermitted));

// Now set up some basic auth handling:

router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));

AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider);

router.route("/eventbus/*").handler(basicAuthHandler);


router.route("/eventbus/*").handler(sockJSHandler);

处理事件总线桥事件

如果您希望在桥上发生事件时收到通知,您可以在呼叫时提供处理程序 bridge

只要桥上发生事件,它就会传递给处理程序。该事件由一个实例描述 BridgeEvent

该事件可以是以下类型之一:

SOCKET_CREATED

当创建新的SockJS套接字时将发生此事件。

SOCKET_IDLE

当SockJS套接字闲置比初始配置更长的时间时,会发生此事件。

SOCKET_PING

当最后的ping时间戳更新为SockJS套接字时,将发生此事件。

SOCKET_CLOSED

当SockJS套接字关闭时,会发生此事件。

发送

当试图将消息从客户端发送到服务器时,将发生此事件。

发布

当试图将消息从客户端发布到服务器时,会发生此事件。

接收

当试图将消息从服​​务器传送到客户端时,会发生此事件。

寄存器

当客户端尝试注册处理程序时会发生此事件。

UNREGISTER

当客户端尝试注销处理程序时,会发生此事件。

该事件使您能够检索使用的类型type并检查事件的原始消息getRawMessage

原始消息是具有以下结构的JSON对象:

{
  “type”:“发送”|“发布”|“接收”|“注册”|“取消注册”,
  “地址”:发送/发布/注册/未注册的事件总线地址
  “body”:消息的主体
}

这个事件也是一个例子Future处理完事件后,您可以完成未来,true以启用进一步处理。

如果你不想让事件得到处理,你可以用来完成未来false这是一个非常有用的功能,可以让您对通过网桥的消息进行自己的过滤,或者应用一些细粒度的授权或指标。

下面是一个例子,我们拒绝所有通过网桥的消息,如果它们包含单词“Armadillos”。

Router router = Router.router(vertx);

// Let through any messages sent to 'demo.orderMgr' from the client
PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.someService");

SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted);

sockJSHandler.bridge(options, be -> {
  if (be.type() == BridgeEventType.PUBLISH || be.type() == BridgeEventType.RECEIVE) {
    if (be.getRawMessage().getString("body").equals("armadillos")) {
      // Reject it
      be.complete(false);
      return;
    }
  }
  be.complete(true);
});

router.route("/eventbus/*").handler(sockJSHandler);

以下是一个如何配置和处理SOCKET_IDLE桥事件类型的示例。注意setPingTimeout(5000),如果ping消息没有在5秒内从客户端到达,那么会触发SOCKET_IDLE桥事件。

Router router = Router.router(vertx);

// Initialize SockJS handler
SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted).setPingTimeout(5000);

sockJSHandler.bridge(options, be -> {
  if (be.type() == BridgeEventType.SOCKET_IDLE) {
    // Do some custom handling...
  }

  be.complete(true);
});

router.route("/eventbus/*").handler(sockJSHandler);

在客户端JavaScript中,您可以使用'vertx-eventbus.js`库创建到事件总线的连接并发送和接收消息:

<script src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
<script src='vertx-eventbus.js'></script>

<script>

var eb = new EventBus('http://localhost:8080/eventbus', {"vertxbus_ping_interval": 300000}); // sends ping every 5 minutes.

eb.onopen = function() {

 // set a handler to receive a message
 eb.registerHandler('some-address', function(error, message) {
   console.log('received a message: ' + JSON.stringify(message));
 });

 // send a message
 eb.send('some-address', {name: 'tim', age: 587});
}

</script>

该示例所做的第一件事是创建事件总线的实例

var eb = new EventBus('http://localhost:8080/eventbus', {"vertxbus_ping_interval": 300000});

构造函数的第二个参数告诉sockjs库每5分钟发送一次ping消息。因为服务器被配置为期望每5秒ping一次→ SOCKET_IDLE将在服务器上触发。

您还可以修改原始消息,例如更改正文。对于从客户端流入的消息,您还可以向消息添加标题,这里是一个示例:

Router router = Router.router(vertx);

// Let through any messages sent to 'demo.orderService' from the client
PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.orderService");

SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted);

sockJSHandler.bridge(options, be -> {
  if (be.type() == BridgeEventType.PUBLISH || be.type() == BridgeEventType.SEND) {
    // Add some headers
    JsonObject headers = new JsonObject().put("header1", "val").put("header2", "val2");
    JsonObject rawMessage = be.getRawMessage();
    rawMessage.put("headers", headers);
    be.setRawMessage(rawMessage);
  }
  be.complete(true);
});

router.route("/eventbus/*").handler(sockJSHandler);

CSRF跨站请求伪造

CSRF或有时也称为XSRF是一种技术,通过该技术,未经授权的站点可以获取用户的私人数据。Vert.x-Web包含一个处理程序CSRFHandler,您可以使用它来防止跨站点请求伪造请求。

在此处理程序下的每个获取请求上,一个cookie将添加到具有唯一标记的响应中。然后期望客户将这个令牌返回回头部。由于发送了cookie,因此要求cookie处理程序也存在于路由器上。

在开发依赖用户代理执行POST操作的非单页应用程序时,无法在HTML表单上指定标题。为了解决这个问题,当且仅当没有标题出现在与标题同名的表单属性中时,标题值也会被检查,例如:

---
<form action="/submit" method="POST">
<input type="hidden" name="X-XSRF-TOKEN" value="abracadabra">
</form>
---

用户有责任为表单域填写正确的值。喜欢使用仅HTML解决方案的用户可以通过从键X-XSRF-TOKEN 名下的路由上下文中获取令牌值或在实例化CSRFHandler对象期间选择的头名称来填充此值

router.route().handler(CookieHandler.create());
router.route().handler(CSRFHandler.create("abracadabra"));
router.route().handler(rc -> {

});

VirtualHost处理程序

虚拟主机处理程序将验证请求主机名,如果它匹配,它将发送请求到注册的处理程序,否则将继续在正常的处理程序链内。

请求将根据Host标头进行检查以进行匹配,并且模式允许使用通配符,例如.vertx.io或完全域名www.vertx.io

router.route().handler(VirtualHostHandler.create("*.vertx.io", routingContext -> {
  // do something if the request is for *.vertx.io
}));

OAuth2AuthHandler处理程序

OAuth2AuthHandler允许使用的OAuth2协议安全的路线快速设置。这个处理程序简化了authCode流程。使用它来保护某些资源并使用Gi​​tHub进行身份验证的示例可以实现为:

OAuth2Auth authProvider = GithubAuth.create(vertx, "CLIENT_ID", "CLIENT_SECRET");

// create a oauth2 handler on our running server
// the second argument is the full url to the callback as you entered in your provider management console.
OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProvider, "https://myserver.com/callback");

// setup the callback handler for receiving the GitHub callback
oauth2.setupCallback(router.route());

// protect everything under /protected
router.route("/protected/*").handler(oauth2);
// mount some handler under the protected zone
router.route("/protected/somepage").handler(rc -> rc.response().end("Welcome to the protected resource!"));

// welcome page
router.get("/").handler(ctx -> ctx.response().putHeader("content-type", "text/html").end("Hello<br><a href=\"/protected/somepage\">Protected by Github</a>"));

OAuth2AuthHandler将设置适当的回调OAuth2处理程序,以便用户不需要处理权威服务器响应的验证。知道权威服务器响应只有一次是非常重要的,这意味着如果客户端发出重新加载的回调URL,它将被认定为无效请求,因为验证将失败。

经验法则是一旦有效的回调被执行,客户端重定向到受保护的资源。此重定向还应创建会话cookie(或其他会话机制),以便用户无需为每个请求进行身份验证。

由于OAuth2规范的性质,为了使用其他OAuth2提供程序,需要稍作更改,但vertx-auth为您提供了许多开箱即用的实现:

但是,如果您使用的是未列出的提供程序,则仍然可以使用以下基本API执行此操作:

OAuth2Auth authProvider = OAuth2Auth.create(vertx, OAuth2FlowType.AUTH_CODE, new OAuth2ClientOptions()
  .setClientID("CLIENT_ID")
  .setClientSecret("CLIENT_SECRET")
  .setSite("https://accounts.google.com")
  .setTokenPath("https://www.googleapis.com/oauth2/v3/token")
  .setAuthorizationPath("/o/oauth2/auth"));

// create a oauth2 handler on our domain: "http://localhost:8080"
OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProvider, "http://localhost:8080");

// these are the scopes
oauth2.addAuthority("profile");

// setup the callback handler for receiving the Google callback
oauth2.setupCallback(router.get("/callback"));

// protect everything under /protected
router.route("/protected/*").handler(oauth2);
// mount some handler under the protected zone
router.route("/protected/somepage").handler(rc -> rc.response().end("Welcome to the protected resource!"));

// welcome page
router.get("/").handler(ctx -> ctx.response().putHeader("content-type", "text/html").end("Hello<br><a href=\"/protected/somepage\">Protected by Google</a>"));

您需要手动提供您提供者的所有详细信息,但最终结果是相同的。

该处理程序将为您的应用程序指定配置的回调URL。用法很简单,为处理程序提供一个路由实例,所有设置都将为您完成。在一个典型的用例中,你的提供者会问你你的应用程序的回调url是什么,然后输入一个url,如:https://myserver.com/callback这是处理程序的第二个参数,现在您只需设置它。为了让最终用户更容易,您只需调用setupCallback方法即可。

这就是你如何将你的处理程序绑定到服务器https://myserver.com:8447/callback请注意,端口号对于默认值不是强制性的,http为80,https为443。

OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(provider, "https://myserver.com:8447/callback");
// now allow the handler to setup the callback url for you
oauth2.setupCallback(router.route());

在这个例子中,Router.route()如果你想完全控制处理程序被调用的顺序(例如你希望尽快在链中调用它),你可以在创建路由对象之前并将其作为此方法的参考。

一个真实世界的例子

到目前为止,您已经学会了如何使用Oauth2 Handler,但您会注意到,对于每个请求,您都需要进行身份验证。这是因为处理程序没有状态,并且在示例中没有应用状态管理。

尽管面向API的端点建议不使用状态,例如,面向终端用户使用JWT(我们将在后面介绍),我们可以将认证结果保存在会话中。为了这个工作,我们需要一个应用程序,如下面的代码片段所示:

router.route()
  .handler(CookieHandler.create());
router.route()
  .handler(SessionHandler.create(LocalSessionStore.create(vertx)));
// Simple auth service which uses a GitHub to
// authenticate the user
OAuth2Auth authProvider =
  GithubAuth.create(vertx, "YOUR PROVIDER CLIENTID", "YOUR PROVIDER CLIENT SECRET");
// We need a user session handler too to make sure
// the user is stored in the session between requests
router.route()
  .handler(UserSessionHandler.create(authProvider));
// we now protect the resource under the path "/protected"
router.route("/protected").handler(
  OAuth2AuthHandler.create(authProvider)
    // we now configure the oauth2 handler, it will
    // setup the callback handler
    // as expected by your oauth2 provider.
    .setupCallback(router.route("/callback"))
    // for this resource we require that users have
    // the authority to retrieve the user emails
    .addAuthority("user:email")
);
// Entry point to the application, this will render
// a custom template.
router.get("/").handler(ctx -> {
  ctx.response()
    .putHeader("Content-Type", "text/html")
    .end(
      "<html>\n" +
      "  <body>\n" +
      "    <p>\n" +
      "      Well, hello there!\n" +
      "    </p>\n" +
      "    <p>\n" +
      "      We're going to the protected resource, if there is no\n" +
      "      user in the session we will talk to the GitHub API. Ready?\n" +
      "      <a href=\"/protected\">Click here</a> to begin!</a>\n" +
      "    </p>\n" +
      "    <p>\n" +
      "      <b>If that link doesn't work</b>, remember to provide\n" +
      "      your own <a href=\"https://github.com/settings/applications/new\">\n" +
      "      Client ID</a>!\n" +
      "    </p>\n" +
      "  </body>\n" +
      "</html>");
});
// The protected resource
router.get("/protected").handler(ctx -> {
  // at this moment your user object should contain the info
  // from the Oauth2 response, since this is a protected resource
  // as specified above in the handler config the user object is never null
  User user = ctx.user();
  // just dump it to the client for demo purposes
  ctx.response().end(user.toString());
});

混合OAuth2和JWT

某些提供商使用JWT令牌作为访问令牌,这是RFC6750的一项功能, 并且在想要混合基于客户端的身份验证和API授权时非常有用。例如,假设您有一个应用程序提供一些受保护的HTML文档,但您也希望它可供API使用。在这种情况下,API无法轻松执行OAuth2所需的重定向握手,但可以使用之前提供的令牌。

只要提供程序配置为支持JWT,就会由处理程序自动处理。

在现实生活中,这意味着您的API可以使用Authorization具有值的标题访问受保护的资源Bearer BASE64_ACCESS_TOKEN

猜你喜欢

转载自blog.csdn.net/zyydecsdn/article/details/80254716