https://vertx.io/docs/vertx-web/java/javascript
Vert.x-Web是一组用于使用Vert.x构建Web应用程序的构建块。将其视为瑞士军刀,用于构建现代,可扩展的网络应用程序。css
Vert.x核心为处理HTTP提供了至关低级别的功能,对于某些应用程序来讲已经足够了。html
Vert.x-Web构建于Vert.x核心之上,能够更轻松地为构建真实Web应用程序提供更丰富的功能。java
Vert.x-Web旨在实现强大,无需激活和彻底嵌入。您只需使用您想要的部件,仅此而已。Vert.x-Web不是容器。github
您可使用Vert.x-Web建立经典的服务器端Web应用程序,RESTful Web应用程序,“实时”(服务器推送)Web应用程序或您能想到的任何其余类型的Web应用程序。Vert.x-Web并不关心。您能够选择本身喜欢的应用程序类型,而不是Vert.x-Web。web
Vert.x-Web很是适合编写有效的HTTP微服务*,但咱们不强迫你编写相似这样的应用程序。ajax
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\n"); // 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\n"); // 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\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/").order(1).handler(routingContext -> { HttpServerResponse response = routingContext.response(); response.write("route1\n"); // 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\n"); // 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>\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" + "