https://vertx.io/docs/vertx-web/java/
Vert.x-Web是一组用于使用Vert.x构建Web应用程序的构建块。将其视为瑞士军刀,用于构建现代,可扩展的网络应用程序。
Vert.x核心为处理HTTP提供了相当低级别的功能,对于某些应用程序来说已经足够了。
Vert.x-Web构建于Vert.x核心之上,可以更轻松地为构建真实Web应用程序提供更丰富的功能。
Vert.x-Web旨在实现强大,无需激活和完全嵌入。您只需使用您想要的部件,仅此而已。Vert.x-Web不是容器。
您可以使用Vert.x-Web创建经典的服务器端Web应用程序,RESTful Web应用程序,“实时”(服务器推送)Web应用程序或您能想到的任何其他类型的Web应用程序。Vert.x-Web并不关心。您可以选择自己喜欢的应用程序类型,而不是Vert.x-Web。
Vert.x-Web非常适合编写有效的HTTP微服务*,但我们不强迫你编写类似这样的应用程序。
Vert.x-Web的一些主要功能包括:
-
路由(基于方法,路径等)
-
路径的正则表达式模式匹配
-
从路径中提取参数
-
内容协商
-
请求身体处理
-
体型限制
-
Cookie解析和处理
-
多部分表格
-
多部分文件上传
-
子路由器
-
会话支持 - 本地(针对粘性会话)和群集(针对非粘性)
-
CORS(跨源资源共享)支持
-
错误页面处理程序
-
基本认证
-
基于重定向的身份验证
-
授权处理程序
-
基于JWT的授权
-
用户/角色/权限授权
-
Favicon处理
-
服务器端呈现的模板支持,包括对开箱即用的以下模板引擎的支持:
-
把手
-
玉,
-
MVEL
-
Thymeleaf
-
Apache FreeMarker
-
卵石
-
摇臂
-
-
响应时间处理程序
-
静态文件服务,包括缓存逻辑和目录列表。
-
请求超时支持
-
SockJS支持
-
事件总线桥
-
CSRF跨站请求伪造
-
虚拟主机
Vert.x-Web中的大多数功能都是作为处理程序实现的,因此您可以随时编写自己的功能。我们设想随着时间的推移写出更多。
我们将在本手册中讨论所有这些功能。
使用Vert.x Web
要使用vert.x web,请将以下依赖项添加到构建描述符的dependencies部分:
-
Maven(在你的
pom.xml
):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>3.8.0</version>
</dependency>
-
Gradle(在您的
build.gradle
文件中):
dependencies {
compile 'io.vertx:vertx-web:3.8.0'
}
发展模式
Vert.x Web默认在生产模式下运行。您可以通过将dev
值分配给以下任一项来切换开发模式:
-
在
VERTXWEB_ENVIRONMENT
环境变量中,或 -
该
vertxweb.environment
系统属性
在开发模式中:
-
模板引擎缓存已禁用
-
在
ErrorHandler
不显示异常详细信息 -
在
StaticHandler
不处理缓存头 -
GraphiQL开发工具已禁用
重新调整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英尺的视图:
路由器接收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).listen(8080);
它基本上与上一节中的Vert.x Core HTTP服务器hello world示例相同,但这次使用的是Vert.x-Web。
我们像以前一样创建HTTP服务器,然后创建路由器。完成后,我们创建一个没有匹配条件的简单路由,以便匹配到达服务器的所有请求。
然后,我们为该路由指定处理程序。将为所有到达服务器的请求调用该处理程序。
传递给处理程序的对象是RoutingContext
- 它包含标准的Vert.x HttpServerRequest
以及HttpServerResponse
其他各种有用的东西,这使得使用Vert.x-Web变得更简单。
对于路由的每个请求,都有一个唯一的路由上下文实例,并且相同的实例将传递给该请求的所有处理程序。
一旦我们设置了处理程序,我们就设置HTTP服务器的请求处理程序以将所有传入的请求传递给handle
。
所以,这是基础知识。现在我们将更详细地研究一下:
处理请求并调用下一个处理程序
当Vert.x-Web决定将请求路由到匹配的路由时,它会调用在实例中传递的路由的处理程序RoutingContext
。路径可以有不同的处理程序,您可以使用它们追加 handler
如果你没有在你的处理程序中结束响应,你应该调用,next
所以另一个匹配的路由可以处理请求(如果有的话)。
next
在处理程序执行完之前,您不必调用。如果你愿意,你可以在以后做一段时间:
Route route = router.route("/some/path/");
route.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
");
// Call the next matching route after a 5 second delay
routingContext.vertx().setTimer(5000, tid -> routingContext.next());
});
route.handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.write("route2
");
// Call the next matching route after a 5 second delay
routingContext.vertx().setTimer(5000, tid -> routingContext.next());
});
route.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 using 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 index param名称可能会很麻烦。可以在正则表达式路径中使用命名捕获组。
Route route = router.routeWithRegex("\/(?<productType>[^\/]+)\/(?<productId>[^\/]+)").handler(routingContext -> {
String productType = routingContext.request().getParam("productType");
String productID = routingContext.request().getParam("productId");
// Do something with them...
});
在上面的示例中,命名捕获组映射到与组同名的路径参数。
此外,您仍然可以像使用普通组一样访问组参数(即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/
});
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
");
// Now call the next matching route
routingContext.next();
});
Route route2 = router.route("/some/path/").handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.write("route2
");
// 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/").order(1).handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.write("route1
");
// Now call the next matching route
routingContext.next();
});
Route route2 = router.route("/some/path/").order(0).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
");
// Now call the next matching route
routingContext.next();
});
Route route3 = router.route("/some/path/").order(2).handler(routingContext -> {
HttpServerResponse response = routingContext.response();
response.write("route3");
// Now end the response
routingContext.response().end();
});
那么响应现在将包含:
路径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
});
如果您没有/
在消费者中指定a ,则会假定您指的是子类型。
基于客户端可接受的MIME类型的路由
HTTP accept
标头用于表示响应的哪些MIME类型是客户端可接受的。
一个accept
报头可具有由分隔的多个MIME类型“”。
MIME类型也可以q
附加一个值*,表示如果有多个响应MIME类型与accept头匹配,则应用加权。q值是介于0和1.0之间的数字。如果省略,则默认为1.0。
例如,以下accept
标头表示客户端将仅接受MIME类型text/plain
:
接受:text / plain
以下客户将接受text/plain
或text/html
不接受。
接受:text / plain,text / html
使用以下内容,客户端将接受text/plain
或text/html
更喜欢,text/html
因为它具有更高的 q
值(默认值为q = 1.0)
接受:text / plain; q = 0.9,text / html
如果服务器可以提供text / plain和text / html,则在这种情况下应该提供text / 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 / * Accept:application / json,text / html Accept: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"
});
上下文数据
您可以使用中的上下文数据RoutingContext
来维护要在请求的生命周期内在处理程序之间共享的任何数据。
这是一个示例,其中一个处理程序在上下文数据中设置一些数据,后续处理程序检索它:
发送到路径的请求/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。
在故障处理程序上也允许重新路由,但是由于重新路由器的性质,当被调用时,当前状态代码和故障原因被重置。为了使重新路由处理程序在需要时生成正确的状态代码,例如:
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请求对URL进行/products/product1234
调用。
但是,假设我们已经拥有另一个路由器所描述的网站:
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.preferredLanguage());
});
main方法acceptableLocales
将返回用户理解的有序语言环境列表,如果您只对用户首选语言环境感兴趣,则帮助程序: preferredLocale
将返回列表的第1个元素,或者null
如果用户未提供语言环境。
路线匹配失败
如果没有任何路由匹配任何特定请求,Vert.x-Web将根据匹配失败发出错误信号:
-
404如果没有路径匹配路径
-
405如果路由与路径匹配但与HTTP方法不匹配
-
406如果路由与路径和方法匹配,但它无法提供具有匹配
Accept
标头的内容类型的响应 -
415如果路径与路径和方法匹配但是它不能接受
Content-type
-
400如果路径与路径和方法匹配,但它不能接受空体
您可以使用手动管理这些故障 errorHandler
错误处理
除了设置处理请求以处理请求之外,您还可以设置处理程序来处理路由中的故障。
故障处理程序使用与您使用普通处理程序完全相同的路径匹配条件。
例如,您可以提供仅处理某些路径上的故障或某些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
。
处理文件上传
正文处理程序还用于处理多部分文件上载。
如果正文处理程序位于请求的匹配路由上,则任何文件上载都将自动流式传输到uploads目录,这是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
实例描述,该实例允许访问各种属性,例如名称,文件名和大小。
处理cookie
Vert.x-Web使用cookies支持cookie CookieHandler
。
对于需要此功能的任何请求,您应确保cookie处理程序位于匹配的路由上。
router.route().handler(CookieHandler.create());
操纵饼干
要删除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独立于商店,这意味着给定来自商店A的会话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提供了两个开箱即用的会话存储实现,如果您愿意,也可以自己编写。
期望实现遵循ServiceLoader
约定,并且将公开从类路径在运行时可用的所有存储。当有多个实现可用时,可以实例化并配置成功的第一个实现成为默认实现。如果没有,则默认值取决于Vert.x的创建模式。如果群集模式可用,则群集会话存储是默认存储,否则本地存储是默认存储。
本地会话商店
使用此存储,会话本地存储在内存中,仅在此实例中可用。
如果您只有一个Vert.x实例在应用程序中使用粘性会话并且已将负载均衡器配置为始终将HTTP请求路由到同一Vert.x实例,则此存储是合适的。
如果您无法确保您的请求都将在同一服务器上终止,则请不要使用此存储,因为您的请求最终可能会出现在不了解您的会话的服务器上。
本地会话存储通过使用共享本地映射来实现,并且具有清除过期会话的收割器。
可以使用带有密钥的json消息配置收割者间隔:reaperInterval
。
以下是创建本地的一些示例 SessionStore
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实例已群集。
以下是创建群集的一些示例 SessionStore
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
。
会话中项目的键始终是字符串。的值可以是任何类型的用于本地会话存储器,并用于一个集群会话存储器它们可以是任何基本类型,或者Buffer
,JsonObject
, 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处理程序
要创建auth处理程序,您需要一个实例AuthProvider
。Auth提供程序用于用户的身份验证和授权。Vert.x在vertx-auth项目中提供了几个开箱即用的auth提供程序实例。有关auth提供程序以及如何使用和配置它们的完整信息,请参阅auth文档。
这是一个在给定auth提供程序的情况下创建基本auth处理程序的简单示例。
router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider);
在您的应用程序中处理auth
假设您希望对以其开头的路径的所有请求都要/private/
进行身份验证。为此,请确保您的auth处理程序位于这些路径上的应用程序处理程序之前:
router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)).setAuthProvider(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;
});
如果AUTH处理程序已成功验证和授权用户将注入一个User
对象入RoutingContext
因此它在你的处理程序可用: user
。
如果您希望将User对象存储在会话中,以便在请求之间可用,这样您就不必对每个请求进行身份验证,那么您应确保在auth之前在匹配的路由上有会话处理程序和用户会话处理程序处理程序。
获得用户对象后,您还可以以编程方式使用其上的方法来授权用户。
如果要使用户注销,可以调用clearUser
路由上下文。
HTTP基本身份验证
HTTP基本身份验证是一种简单的身份验证方法,适用于简单的应用程序。
使用基本身份验证,凭据将在HTTP标头中通过线路以非加密方式发送,因此使用HTTPS而非HTTP来提供应用程序至关重要。
使用基本身份验证,如果用户请求需要授权的资源,则基本身份验证处理程序将发回401
带有标头WWW-Authenticate
集的响应。这会提示浏览器显示登录对话框并提示用户输入其用户名和密码。
再次请求资源,这次使用Authorization
标头集,包含在Base64中编码的用户名和密码。
当基本身份验证处理程序收到此信息时,它会AuthProvider
使用用户名和密码调用配置对用户进行身份验证。如果验证成功,则处理程序尝试授权用户。如果成功,则允许请求的路由继续到应用程序处理程序,否则403
返回响应以表示拒绝访问。
可以使用访问要授予的资源所需的一组权限来设置auth处理程序。
重定向auth处理程序
使用重定向身份验证处理时,如果用户尝试访问受保护资源并且未登录,则会将用户重定向到登录页面。
然后,用户填写登录表单并提交。这由对用户进行身份验证的服务器处理,如果经过身份验证,则将用户重定向回原始资源。
要使用重定向身份验证,您需要配置实例RedirectAuthHandler
而不是基本身份验证处理程序。
您还需要设置处理程序以提供实际的登录页面,以及处理实际登录本身的处理程序。为了处理登录,我们为此提供了一个预构建的处理程序FormLoginHandler
。
这是一个简单应用程序的示例,在默认重定向URL上使用重定向auth处理程序/loginpage
。
router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)).setAuthProvider(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授权
使用JWT授权可以通过权限保护资源,而没有足够权限的用户将被拒绝访问。您需要添加io.vertx:vertx-auth-jwt:3.8.0
要使用的依赖项JWTAuthProvider
要使用此处理程序,需要执行以下两个步骤:
-
设置处理程序以发出令牌(或依赖第三方)
-
设置处理程序以过滤请求
请注意,这两个处理程序应仅在HTTPS上可用,不这样做可以嗅探传输中的令牌,从而导致会话劫持攻击。
这是一个关于如何发出令牌的示例:
Router router = Router.router(vertx);
JWTAuthOptions authConfig = new JWTAuthOptions()
.setKeyStore(new KeyStoreOptions()
.setType("jceks")
.setPath("keystore.jceks")
.setPassword("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);
}
});
既然你的客户端有一个令牌,那么所需要的是forall *后续请求,HTTP头 Authorization
被填充:Bearer <token>
例如:
Router router = Router.router(vertx);
JWTAuthOptions authConfig = new JWTAuthOptions()
.setKeyStore(new KeyStoreOptions()
.setType("jceks")
.setPath("keystore.jceks")
.setPassword("secret"));
JWTAuth authProvider = JWTAuth.create(vertx, authConfig);
router.route("/protected/*").handler(JWTAuthHandler.create(authProvider));
router.route("/protected/somepage").handler(ctx -> {
// some handle code...
});
JWT允许您将任何您喜欢的信息添加到令牌本身。通过执行此操作,服务器中没有允许您扩展应用程序而无需群集会话数据的状态。为了向令牌添加数据,在创建令牌期间只需将数据添加到JsonObject参数:
JWTAuthOptions authConfig = new JWTAuthOptions()
.setKeyStore(new KeyStoreOptions()
.setType("jceks")
.setPath("keystore.jceks")
.setPassword("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,因此如果身份验证在链的给定处理程序中有效,则将使用相同的处理程序执行授权(如果请求)。
重要的是要知道某些处理程序需要特定的提供程序,例如:
因此,预计不会在所有处理程序之间共享提供程序。有些情况下,可以跨处理程序共享提供程序,例如:
-
该
BasicAuthHandler
可以采取任何提供商。 -
该
RedirectAuthHandler
可以采取任何提供商。
所以说你要创建一个同时接受HTTP Basic Authentication
和的应用程序Form 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
标头的请求时,这意味着链将无法使用基本auth处理程序进行身份验证,并将尝试使用重定向处理程序进行身份验证。由于重定向处理程序始终重定向,因此您将被发送到您在该处理程序中配置的登录表单。
与vertx-web中的正常路由一样,auth chaning是一个序列,因此如果您希望回退到浏览器,使用HTTP Basic身份验证而不是重定向来请求用户凭据,则只需要反转附加的顺序。连锁,链条。
现在假设您在提供Authorization
带有值的标头的位置发出请求Basic [token]
。在这种情况下,基本的auth处理程序将尝试进行身份验证,如果它成功,链将停止并且vertx-web将继续处理您的处理程序。如果令牌无效,例如错误的用户名/密码,则链将继续到以下条目。在这种特定情况下,重定向auth处理程序。
提供静态资源
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-control
,last-modified
和date
。
cache-control
max-age=86400
默认设置为。这相当于一天。setMaxAgeSeconds
如果需要,可以配置它 。
如果浏览器发送带有if-modified-since
标头的GET或HEAD请求,并且该资源自该日期起未被修改,304
则返回状态,告知浏览器使用其本地缓存的资源。
如果不需要处理缓存头,则可以禁用它setCachingEnabled
。
启用缓存处理后,Vert.x-Web将缓存内存中资源的最后修改日期,这样可以避免磁盘命中每次都检查实际的上次修改日期。
缓存中的条目具有到期时间,在此之后,将再次检查磁盘上的文件并更新缓存条目。
如果您知道您的文件永远不会在磁盘上更改,那么缓存条目将永远不会过期。这是默认值。
如果您知道在服务器运行时您的文件可能在磁盘上发生更改,那么您可以将只读文件设置为false setFilesReadOnly
。
要在任何时候启用可以在内存中缓存的最大条目数,您可以使用 setMaxCacheSize
。
要配置可以使用的缓存条目的到期时间setCacheEntryTimeout
。
配置索引页面
对根路径的任何请求/
都将导致索引页面被提供。默认情况下,索引页面是index.html
。这可以配置setIndexPage
。
更改Web根目录
默认情况下,将从目录提供静态资源webroot
。配置此用途 setWebRoot
。
目录列表
服务器还可以执行目录列表。默认情况下,禁用目录列表。要启用它setDirectoryListing
。
启用目录列表时,返回的内容取决于accept
标头中的内容类型。
对于text/html
目录列表,可以使用用于呈现目录列表页面的模板进行配置setDirectoryTemplate
。
禁用磁盘上的文件缓存
默认情况下,Vert.x会将从类路径提供的文件缓存到磁盘上的文件中,该文件位于.vertx
当前工作目录中调用的目录的子目录中。这在将服务部署为生产中的fatjars时非常有用,每次从类路径提供文件都很慢。
在开发过程中,这可能会导致问题,就像在服务器运行时更新静态内容一样,缓存文件将不会提供更新的文件。
要禁用文件缓存可以提供您vert.x选项的属性fileResolverCachingEnabled
来false
。为了向后兼容,它还会将该值默认为系统属性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
默认内容类型的呈现结果。这也可以配置。
创建模板处理程序时,您将传入所需模板引擎的实例。模板引擎未嵌入到vertx-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.8.0
。使用以下命令创建MVEL模板引擎的实例:io.vertx.ext.web.templ.MVELTemplateEngine#create()
使用MVEL模板引擎时,.templ
如果文件名中未指定扩展名,它将默认查找带扩展名的模板。
路由上下文RoutingContext
在MVEL模板中可用作context
变量,这意味着您可以基于上下文中的任何内容(包括请求,响应,会话或上下文数据)来呈现模板。
这里有些例子:
请求路径是@ {context.request()。path()} 会话中的变量'foo'是@ {context.session()。get('foo')} 上下文数据中的值“bar”是@ {context.get('bar')}
玉模板引擎
要使用Jade模板引擎,您需要将以下依赖项添加到项目中: io.vertx:vertx-web-templ-jade:3.8.0
。使用以下方法创建Jade模板引擎的实例:io.vertx.ext.web.templ.JadeTemplateEngine#create()
。
使用Jade模板引擎时,.jade
如果文件名中未指定扩展名,它将默认查找带扩展名的模板。
路由上下文RoutingContext
在Jade模板中可用作context
变量,这意味着您可以基于上下文中的任何内容(包括请求,响应,会话或上下文数据)来呈现模板。
这里有些例子:
!五 HTML 头 title = context.get('foo')+ context.request()。path() 身体
把手模板引擎
要使用Handlebars,您需要将以下依赖项添加到项目中: io.vertx:vertx-web-templ-handlebars:3.8.0
。使用以下方法创建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.8.0
。使用以下方法创建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.8.0
。使用以下方法创建Apache FreeMarker模板引擎的实例:io.vertx.ext.web.templ.Engine#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.8.0
。使用以下方法创建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> [剪断]
摇杆模板引擎
要使用Rocker,请将其io.vertx:vertx-web-templ-rocker:3.8.0
作为依赖项添加到项目中。然后,您可以使用创建Rocker模板引擎实例io.vertx.ext.web.templ.rocker#create()
。
然后,传递给render
方法的JSON上下文对象的值将作为模板参数公开。鉴于:
[剪断] final JsonObject context = new JsonObject() .put(“foo”,“badger”) .put(“bar”,“fox”) .put(“context”,new JsonObject()。put(“path”,“/ foo / bar”)); engine.render(context,“somedir / TestRockerTemplate2”,render - > { //(...) }); [剪断]
然后模板可以作为以下somedir/TestRockerTemplate2.rocker.html
资源文件:
@import io.vertx.core.json.JsonObject @args(JsonObject context,String foo,String bar) 你好@foo和@bar 请求路径是@ context.getString(“path”)
禁用缓存
在开发期间,您可能希望禁用模板缓存,以便在每个请求上重新评估模板。为此,您需要设置系统属性:io.vertx.ext.web.TemplateEngine.disableCache
to true
。
默认情况下,它将为false。因此始终启用缓存。
错误处理程序
您可以使用模板处理程序或其他方式呈现自己的错误,但Vert.x-Web还包含一个可以为您呈现错误页面的四四方方的“漂亮”错误处理程序。
处理程序是ErrorHandler
。要使用错误处理程序,只需将其设置为您想要覆盖的任何路径的失败处理程序。
请求记录器
Vert.x-Web包含一个LoggerHandler
可用于记录HTTP请求的处理程序。您应该在任何可能失败的处理程序之前安装此处理程序RoutingContext
默认情况下,请求会记录到Vert.x记录器,该记录器可以配置为使用JUL日志记录,log4j或SLF4J。
提供favicon
Vert.x-Web包含FaviconHandler
特别用于服务favicons 的处理程序。
可以使用文件系统的路径指定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
响应标头,其中包含从接收请求到写入响应标头的时间(以毫秒为单位),例如:
x响应时间: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服务器,而不管实际的浏览器或网络是否允许真正的WebSockets。
它通过支持浏览器和服务器之间的各种不同传输,并根据浏览器和网络功能在运行时选择一个来实现这一点。
所有这些对您来说都是透明的 - 您只需使用类似WebSocket的界面即可。
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连接时都会调用它:
传递给处理程序的对象是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 = function(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连接来隧道传输事件总线流量。
然后在其SockJSHandler
上安装一个特殊的SockJS套接字处理程序,它处理SockJS数据并将其与服务器端事件总线桥接。
要激活网桥,只需调用 bridge
SockJS处理程序即可。
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.8.0</version>
<classifier>client</classifier>
<type>js</type>
</dependency>
-
Gradle(在您的
build.gradle
文件中):
compile 'io.vertx:vertx-web:3.8.0:client'
该图书馆也可用于:
请注意,API已在3.0.0和3.1.0版本之间进行了更改。请检查更改日志。以前的客户端仍然兼容,仍然可以使用,但新客户端提供更多功能,并且更接近vert.x事件总线API。
保护桥梁
如果您在没有保护它的情况下启动了上述示例中的桥接器,并尝试通过它发送消息,您会发现消息神秘地消失了。他们发生了什么?
对于大多数应用程序,您可能不希望客户端JavaScript能够向服务器端的任何处理程序或所有其他浏览器发送任何消息。
例如,您可能在事件总线上有一个服务,允许访问或删除数据。我们不希望行为不端或恶意的客户端能够删除数据库中的所有数据!
此外,我们不一定希望任何客户端能够监听任何事件总线地址。
为了解决这个问题,SockJS桥将默认拒绝通过任何消息。您可以告诉桥接器哪些消息可以通过。(对于总是允许通过的回复消息,有一个例外)。
换句话说,网桥就像一种具有默认拒绝所有策略的防火墙。
配置网桥告诉它应该通过哪些消息很容易。
您可以使用在调用bridge时传入的内容来指定要允许入站和出站流量的 匹配项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 auth处理程序。例如:
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
-
当为SockJS套接字更新最后一个ping时间戳时,将发生此事件。
- SOCKET_CLOSED
-
当SockJS套接字关闭时,将发生此事件。
- 发送
-
当尝试从客户端向服务器发送消息时,将发生此事件。
- 发布
-
尝试从客户端向服务器发布消息时,将发生此事件。
- 接收
-
当尝试将消息从服务器传递到客户端时,将发生此事件。
- 寄存器
-
当客户端尝试注册处理程序时,将发生此事件。
- UNREGISTER
-
当客户端尝试取消注册处理程序时,将发生此事件。
该事件使您可以使用type
并检查事件的原始消息来检索类型getRawMessage
。
原始消息是具有以下结构的JSON对象:
{ “type”:“send”|“publish”|“receive”|“register”|“unregister”, “地址”:发送/发布/注册/未注册的事件总线地址 “身体”:信息的主体 }
该事件也是一个例子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秒SOCKET_IDLE
就会发生一次ping→ 将在服务器上触发。
您还可以修改原始消息,例如更改正文。对于从客户端流入的消息,您还可以向消息添加标头,这是一个示例:
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
,可用于防止跨站点请求伪造请求。
在此处理程序下的每个get请求中,cookie将使用唯一标记添加到响应中。然后,客户端需要将此令牌返回到标头中。由于cookie被发送,因此要求cookie处理程序也存在于路由器上。
在开发依赖User-Agent执行POST
操作的非单页应用程序时,无法在HTML Forms上指定Headers。为了解决这个问题,当且仅当在与表头名称相同的表单属性中不存在标题时,还将检查标题值,例如:
---
<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 -> {
});
使用AJAX
当通过ajax访问受保护的路由时,需要在请求中传递csrf令牌。通常,这是使用请求标头完成的,因为添加请求标头通常可以在中央位置轻松完成,而无需修改负载。
CSRF令牌是从密钥下的服务器端上下文获取的X-XSRF-TOKEN
(除非您指定了不同的名称)。需要将此令牌暴露给客户端,通常是将其包含在初始页面内容中。一种可能性是将其存储在HTML <meta>标记中,然后可以在JavaScript请求时检索值。
以下内容可以包含在您的视图中(下面的车把示例):
<meta name="csrf-token" content="${X-XSRF-TOKEN}">
以下是使用Fetch API使用页面上<meta>标记中的CSRF令牌发布到/ process路由的示例:
// Read the CSRF token from the <meta> tag
var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
// Make a request using the Fetch API
fetch('/process', {
credentials: 'same-origin', // <-- includes cookies in the request
headers: {
'X-XSRF-TOKEN': token // <-- is the csrf token as a header
},
method: 'POST',
body: {
key: 'value'
}
})
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流程。使用它来保护某些资源并使用GitHub进行身份验证的示例可以实现为:
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为您提供了许多开箱即用的实现:
-
Azure Active Directory
AzureADAuth
-
Box.com
BoxAuth
-
Dropbox的
DropboxAuth
-
Facebook的
FacebookAuth
-
Github上
GithubAuth
-
谷歌
GoogleAuth
-
Instagram的
InstagramAuth
-
Keycloak
KeycloakAuth
-
LinkedIn
LinkedInAuth
-
Mailchimp
MailchimpAuth
-
销售队伍
SalesforceAuth
-
Shopify
ShopifyAuth
-
的SoundCloud
SoundcloudAuth
-
条纹
StripeAuth
-
推特
TwitterAuth
但是,如果您使用的是不公开的提供程序,您仍然可以使用基本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。用法很简单,因为为处理程序提供路由实例,所有设置都将为您完成。在典型的用例中,您的提供商会询问您的应用程序的回调网址是什么,然后输入以下网址: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的端点,建议不使用状态,例如,对于面向用户的endpoinst使用JWT(我们将在后面介绍),我们可以将身份验证结果保存在会话中。为此,我们需要一个类似以下代码段的应用程序:
router.route()
.handler(CookieHandler.create());
// 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(SessionHandler.create(LocalSessionStore.create(vertx)).setAuthProvider(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>
" +
" <body>
" +
" <p>
" +
" Well, hello there!
" +
" </p>
" +
" <p>
" +
" We're going to the protected resource, if there is no
" +
" user in the session we will talk to the GitHub API. Ready?
" +
" <a href="/protected">Click here</a> to begin!</a>
" +
" </p>
" +
" <p>
" +
" <b>If that link doesn't work</b>, remember to provide
" +
" your own <a href="https://github.com/settings/applications/new">
" +
" Client ID</a>!
" +
" </p>
" +