Vert.x Web 文檔手冊

Vert.x Web

中英對照表

  • Container:容器
  • Micro-service:微服務
  • Bridge:橋接
  • Router:路由器
  • Route:路由
  • Sub-Route: 子路由
  • Handler:處理器,某些特定的地方未翻譯
  • Blocking:阻塞式
  • Context:上下文。非特別說明指代路由的上下文 routing context,不一樣於 Vert.x core 的 Context
  • Application:應用
  • Header:消息頭
  • Body:消息體
  • MIME types:互聯網媒體類型
  • Load-Balancer:負載均衡器
  • Socket:套接字
  • Mount:掛載

組件介紹

Vert.x Web 是一系列用於基於 Vert.x 構建 Web 應用的構建模塊。css

能夠把它想象成一把構建現代的、可伸縮的 Web 應用的瑞士軍刀。html

Vert.x Core 提供了一系列底層的功能用於操做 HTTP,對於一部分應用來是足夠的。java

Vert.x Web 基於 Vert.x Core,提供了一系列更豐富的功能以便更容易地開發實際的 Web 應用。git

它繼承了 Vert.x 2.x 裏的 Yoke 的特色,靈感來自於 Node.js 的框架 Express 和 Ruby 的框架 Sinatra 等等。github

Vert.x Web 的設計是強大的,非侵入式的,而且是徹底可插拔的。Vert.x Web 不是一個容器,您能夠只使用您須要的部分。web

您可使用 Vert.x Web 來構建經典的服務端 Web 應用、RESTful 應用、實時的(服務端推送)Web 應用,或任何類型的您所能想到的 Web 應用。應用類型的選擇取決於您,而不是 Vert.x Web。正則表達式

Vert.x Web 很是適合編寫 RESTful HTTP 微服務,但咱們不強制您必須把應用實現成這樣。算法

Vert.x Web 的一部分關鍵特性有:數據庫

  • 路由(基於方法(1)、路徑等)
  • 基於正則表達式的路徑匹配
  • 從路徑中提取參數
  • 內容協商(2)
  • 處理消息體
  • 消息體的長度限制
  • 接收和解析 Cookie
  • Multipart 表單
  • Multipart 文件上傳
  • 子路由
  • 支持本地會話和集羣會話
  • 支持 CORS(跨域資源共享)
  • 錯誤頁面處理器
  • HTTP基本認證
  • 基於重定向的認證
  • 受權處理器
  • 基於 JWT 的受權
  • 用戶/角色/權限受權
  • 網頁圖標處理器
  • 支持服務端模板渲染,包括如下開箱即用的模板引擎:
    • Handlebars
    • Jade
    • MVEL
    • Thymeleaf
    • Apache FreeMarker
    • Pebble
  • 響應時間處理器
  • 靜態文件服務,包括緩存邏輯以及目錄監聽
  • 支持請求超時
  • 支持 SockJS
  • 橋接 Event-bus
  • CSRF 跨域請求僞造
  • 虛擬主機

Vert.x Web 的大多數特性被實現爲了處理器(Handler),所以您隨時能夠實現您本身的處理器。咱們預計隨着時間的推移會有更多的處理器被實現。express

咱們會在本手冊裏討論全部上述的特性。

使用 Vert.x Web

在使用 Vert.x Web 以前,須要爲您的構建工具在描述文件中添加依賴項:

  • Maven(在 pom.xml 文件中):
<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-web</artifactId>
  <version>3.4.2</version>
</dependency>
  • Gradle(在 build.gradle 文件中):
dependencies {
  compile 'io.vertx:vertx-web:3.4.2'
}

回顧 Vert.x Core 的 HTTP 服務端

Vert.x Web 使用了 Vert.x Core 暴露的 API,因此熟悉基於 Vert.x Core 編寫 HTTP 服務端的基本概念是頗有價值的。

Vert.x Core 的 HTTP 文檔 有不少關於這方面的細節。

下面是一個使用 Vert.x Core 編寫的 Hello World Web 服務器,暫不涉及 Vert.x Web:

HttpServer server = vertx.createHttpServer();

server.requestHandler(request -> {

  // 全部的請求都會調用這個處理器處理
  HttpServerResponse response = request.response();
  response.putHeader("content-type", "text/plain");

  // 寫入響應並結束處理
  response.end("Hello World!");
});

server.listen(8080);

咱們建立了一個 HTTP 服務端,並設置了一個請求處理器。全部的請求都會調用這個處理器處理。

當請求到達時,咱們設置了響應的 Content Type 爲 text/plain 並寫入了 Hello World! 而後結束了處理。

以後,咱們告訴服務器監聽 8080 端口(默認的主機名是 localhost)。

您能夠執行這段代碼,並打開瀏覽器訪問 http://localhost:8080 來驗證它是否如預期的同樣工做。

Vert.x Web 的基本概念

Router 是 Vert.x Web 的核心概念之一。它是一個維護了零或多個 Route 的對象。

Router 接收 HTTP 請求,並查找首個匹配該請求的 Route,而後將請求傳遞給這個 Route

Route 能夠持有一個與之關聯的處理器用於接收請求。您能夠經過這個處理器對請求作一些事情,而後結束響應或者把請求傳遞給下一個匹配的處理器。

如下是一個簡單的路由示例:

HttpServer server = vertx.createHttpServer();

Router router = Router.router(vertx);

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

  // 全部的請求都會調用這個處理器處理
  HttpServerResponse response = routingContext.response();
  response.putHeader("content-type", "text/plain");

  // 寫入響應並結束處理
  response.end("Hello World from Vert.x-Web!");
});

server.requestHandler(router::accept).listen(8080);
HttpServer server = vertx.createHttpServer();

Router router = Router.router(vertx);

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

  // 全部的請求都會調用這個處理器處理
  HttpServerResponse response = routingContext.response();
  response.putHeader("content-type", "text/plain");

  // 寫入響應並結束處理
  response.end("Hello World from Vert.x-Web!");
});

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

它作了和上文使用 Vert.x Core 實現的 HTTP 服務器基本相同的事情,只是這一次換成了 Vert.x Web。

和上文同樣,咱們建立了一個 HTTP 服務器,而後建立了一個 Router。在這以後,咱們建立了一個沒有匹配條件的 Route,這個 route 會匹配全部到達這個服務器的請求。

以後,咱們爲這個 route 指定了一個處理器,全部的請求都會調用這個處理器處理。

調用處理器的參數是一個 RoutingContext 對象。它不只包含了 Vert.x 中標準的 HttpServerRequest 和HttpServerResponse,還包含了各類用於簡化 Vert.x Web 使用的東西。

每個被路由的請求對應一個惟一的 RoutingContext,這個實例會被傳遞到全部處理這個請求的處理器上。

當咱們建立了處理器以後,咱們設置了 HTTP 服務器的請求處理器,使全部的請求都經過 accept(3)處理。

這些是最基本的,下面咱們來看一下更多的細節:

處理請求並調用下一個處理器

當 Vert.x Web 決定路由一個請求到匹配的 route 上,它會使用一個 RoutingContext 調用對應處理器。

若是您不在處理器裏結束這個響應,您須要調用 next 方法讓其餘匹配的 Route 來處理請求(若是有)。

您不須要在處理器執行完畢時調用 next 方法。您能夠在以後您須要的時間點調用它:

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

  HttpServerResponse response = routingContext.response();
  // 因爲咱們會在不一樣的處理器裏寫入響應,所以須要啓用分塊傳輸
  // 僅當須要經過多個處理器輸出響應時才須要
  response.setChunked(true);

  response.write("route1\n");

  // 5 秒後調用下一個處理器
  routingContext.vertx().setTimer(5000, tid -> routingContext.next());
});

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

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

  // 5 秒後調用下一個處理器
  routingContext.vertx().setTimer(5000, tid ->  routingContext.next());
});

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

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

  // 結束響應
  routingContext.response().end();
});

在上述的例子中,route1 向響應裏寫入了數據,5秒以後 route2 向響應裏寫入了數據,再5秒以後 route3 向響應裏寫入了數據並結束了響應。

注意,全部發生的這些沒有線程阻塞。

使用阻塞式處理器

某些時候您可能須要在處理器裏執行一些須要阻塞 Event Loop 的操做,好比調用某個傳統的阻塞式 API 或者執行密集計算。

您不能在普通的處理器裏執行這些操做,因此咱們提供了向 Route 設置阻塞式處理器的能力。

阻塞式處理器和普通處理器的區別是 Vert.x 會使用 Worker Pool 中的線程而不是 Event Loop 線程來處理請求。

您可使用 blockingHandler 方法來設置阻塞式處理器。下面是一個例子:

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

  // 執行某些同步的耗時操做
  service.doSomethingThatBlocks();

  // 調用下一個處理器
  routingContext.next();

});

默認狀況下在一個 Context(Vert.x Core 的 Context,例如同一個 Verticle 實例) 上執行的全部阻塞式處理器的執行是順序的,也就意味着只有一個處理器執行完了纔會繼續執行下一個。 若是您不關心執行的順序,而且不介意阻塞式處理器以並行的方式執行,您能夠在調用 blockingHandler 方法時將 ordered 設置爲 false

注意,若是您須要在一個阻塞處理器中處理一個 multipart 類型的表單數據,您須要首先使用一個非阻塞的處理器來調用 setExpectMultipart(true) 下面是一個例子:

router.post("/some/endpoint").handler(ctx -> {
  ctx.request().setExpectMultipart(true);
  ctx.next();
}).blockingHandler(ctx -> {
  // 執行某些阻塞操做
});

基於精確路徑的路由

能夠將 Route 設置爲只匹配指定的 URI。在這種狀況下它只會匹配路徑和該路徑一致的請求。

在下面這個例子中會被路徑爲 /some/path/ 的請求調用。咱們會忽略結尾的 /,因此路徑 /some/path或者 /some/path// 的請求也是匹配的:

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

route.handler(routingContext -> {
  // 全部如下路徑的請求都會調用這個處理器:

  // `/some/path`
  // `/some/path/`
  // `/some/path//`
  //
  // 但不包括:
  // `/some/path/subdir`
});

基於路徑前綴的路由

您常常須要爲全部以某些路徑開始的請求設置 Route。您可使用正則表達式來實現,但更簡單的方式是在聲明 Route 的路徑時使用一個 * 做爲結尾。

在下面的例子中處理器會匹配全部 URI 以 /some/path 開頭的請求。

例如 /some/path/foo.html 和 /some/path/otherdir/blah.css 都會匹配。

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

route.handler(routingContext -> {
  // 全部路徑以 `/some/path/` 開頭的請求都會調用這個處理器處理,例如:

  // `/some/path`
  // `/some/path/`
  // `/some/path/subdir`
  // `/some/path/subdir/blah.html`
  //
  // 但不包括:
  // `/some/bath`
});

也能夠在建立 Route 的時候指定任意的路徑:

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

route.handler(routingContext -> {
  // 這個路由器的調用規則和上面的例子同樣
});

捕捉路徑參數

能夠經過佔位符聲明路徑參數並在處理請求時經過 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");

  // 執行某些操做...
});

佔位符由 : 和參數名構成。參數名由字母、數字和下劃線構成。

在上述的例子中,若是一個 POST 請求的路徑爲 /catalogue/products/tools/drill123/,那麼會匹配這個 Route,而且會接收到參數 productType 的值爲 tools,參數 productID 的值爲 drill123

基於正則表達式的路由

正則表達式一樣也可用於在路由時匹配 URI 路徑。

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

route.handler(routingContext -> {

  // 如下路徑的請求都會調用這個處理器:

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

  // 但不包括:
  // /bar/wibble
});

或者在建立 Route 時指定正則表達式:

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

route.handler(routingContext -> {

  // 這個路由器的調用規則和上面的例子同樣

});

經過正則表達式捕捉路徑參數

您也能夠捕捉經過正則表達式聲明的路徑參數,下面是一個例子:

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

// 這個正則表達式能夠匹配路徑相似於 `/foo/bar` 的請求
// `foo` 能夠經過參數 param0 獲取,`bar` 能夠經過參數 param1 獲取
route.pathRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(routingContext -> {

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

  // 執行某些操做
});

在上面的例子中,若是一個請求的路徑爲 /tools/drill123/,那麼會匹配這個 route,而且會接收到參數 productType 的值爲 tools,參數 productID 的值爲 drill123

基於 HTTP Method 的路由

默認的,Route 會匹配全部 HTTP Method。

若是您須要 Route 只匹配指定的 HTTP Method,您可使用 method 方法。

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

route.handler(routingContext -> {

  // 全部的 POST 請求都會調用這個處理器

});

或者能夠在建立這個 Route 時和路徑一塊兒指定:

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

route.handler(routingContext -> {

  // 全部路徑爲 `/some/path/` 的 POST 請求都會調用這個處理器

});

若是您想讓 Route 指定的 HTTP Method ,您也可使用對應的 getpostput 等方法。下面是一個例子:

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

  // 全部 GET 請求都會調用這個處理器

});

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

  // 全部路徑爲 `/some/path/` 的 GET 請求都會調用這個處理器

});

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

  // 全部路徑以 `foo` 結尾的 GET 請求都會調用這個處理器

});

若是您想要讓一個路由匹配不止一個 HTTP Method,您能夠調用 method 方法屢次:

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

route.handler(routingContext -> {

  // 全部 GET 或 POST 請求都會調用這個處理器

});

路由順序

默認的路由的匹配順序與添加到 Router 的順序一致。

當一個請求到達時,Router 會一步一步檢查每個 Route 是否匹配,若是匹配則對應的處理器會被調用。

若是處理器隨後調用了 next,則下一個匹配的 Route 對應的處理器(若是有)會被調用,以此類推。

下面的例子展現了這個過程:

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

  HttpServerResponse response = routingContext.response();
  // 因爲咱們會在不一樣的處理器裏寫入響應,所以須要啓用分塊傳輸
  // 僅當須要經過多個處理器輸出響應時才須要
  response.setChunked(true);

  response.write("route1\n");

  // 調用下一個匹配的 route
  routingContext.next();
});

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

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

  // 調用下一個匹配的 route
  routingContext.next();
});

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

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

  // 結束響應
  routingContext.response().end();
});

在上面的例子裏,響應中會包含:

route1
route2
route3

對於任意以 /some/path 開頭的請求,Route會被依次調用。

若是您想覆蓋路由默認的順序,您能夠經過 order 方法爲每個路由指定一個 integer 值。

當 Route 被建立時 order 會被賦值爲其被添加到 Router 時的序號,例如第一個 Route 是 0,第二個是 1,以此類推。

您可使用特定的順序值覆蓋默認的順序。若是您須要確保一個 Route 在順序 0 的 Route 以前執行,能夠將其指定爲負值。

讓咱們改變 route2 的值使其能在 route1 以前執行:

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

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

  // 調用下一個匹配的 route
  routingContext.next();
});

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

  HttpServerResponse response = routingContext.response();
  // 因爲咱們會在不一樣的處理器裏寫入響應,所以須要啓用分塊傳輸
  // 僅當須要經過多個處理器輸出響應時才須要
  response.setChunked(true);

  response.write("route2\n");

  // 調用下一個匹配的 route
  routingContext.next();
});

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

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

  // 結束響應
  routingContext.response().end();
});

// 更改 route2 的順序使其能夠在 route1 以前執行
route2.order(-1);

此時響應內容會是:

route2
route1
route3

若是兩個匹配的 Route 有相同的順序值,則會按照添加它們的順序來調用。

您也能夠經過 last 方法來指定 Route 最後執行。

基於請求媒體類型(MIME types)的路由

您可使用 consumes 方法指定 Route 匹配對應 MIME 類型的請求。

在這種狀況下,若是請求中包含了消息頭 content-type 聲明瞭消息體的 MIME 類型。則它會與經過 consumes 方法聲明的值進行比較。

通常來講,consumes 描述了處理器可以處理的 MIME 類型。

MIME Type 的匹配過程是精確的:

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

  // 全部 `content-type` 消息頭的值爲 `text/html` 的請求會調用這個處理器

});

也能夠匹配多個精確的值(MIME 類型):

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

  // 全部 `content-type` 消息頭的值爲 `text/html` 或 `text/plain` 的請求會調用這個處理器

});

基於通配符的子類型匹配也是支持的:

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

  // 全部 `content-type` 消息頭的頂級類型爲 `text` 的請求會調用這個處理器
  // 例如 `content-type` 消息頭設置爲 `text/html` 或 `text/plain` 都會匹配

});

您也能夠用通配符匹配頂級的類型(top level type):

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

  // 全部 `content-type` 消息頭的子類型爲 `json` 的請求會調用這個處理器
  // 例如 `content-type` 消息頭設置爲 `text/json` 或 `application/json` 都會匹配

});

若是您沒有在 consumers 中包含 /,則意味着是一個子類型(sub-type)。

基於客戶端可接受媒體類型(MIME types acceptable)的路由

HTTP 的 accept 消息頭用於表示哪些 MIME 類型的響應是客戶端可接受的。

一個 accept 消息頭能夠包含多個用 , 分隔的 MIME 類型。

若是在 accept 消息頭中匹配了不止一個 MIME 類型,則能夠爲每個 MIME 類型追加一個 q 值來表示權重。q 的取值範圍由 0 到 1.0。缺省值爲 1.0。

例如,下面的 accept 消息頭表示客戶端只接受 text/plain 類型的響應。

Accept: text/plain

如下 accept 表示客戶端會無偏好地接受 text/plain 或 text/html

Accept: text/plain, text/html

如下 accept 表示客戶端會接受 text/plain 或 text/html,但會更傾向於 text/html,由於其具備更高的 q 值(默認值爲 1.0)。

Accept: text/plain; q=0.9, text/html

在這種狀況下,若是服務器能夠同時提供 text/plain 和 text/html,它須要提供 text/html

您可使用 produces 來定義 Route 能夠提供哪些 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();

});

在這種狀況下這個 Route 會匹配任何 accept 消息頭匹配 application/json 的請求。例如:

Accept: application/json
Accept: application/*
Accept: application/json, text/html
Accept: application/json;q=0.7, text/html;q=0.8, text/plain

您也能夠標記您的 Route 提供不止一種 MIME 類型。在這種狀況下,您可使用 getAcceptableContentType 方法來找出真正被接受的 MIME 類型。

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

  HttpServerResponse response = routingContext.response();

  // 獲取最終匹配到的 MIME type
  String acceptableContentType = routingContext.getAcceptableContentType();

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

在上述例子中,若是您發送一個包含以下 accept 消息頭的請求:

Accept: application/json; q=0.7, text/html

那麼會匹配上面的 Route,而且 acceptableContentType 的值會是 text/html 由於其具備更高的 q值。

組合路由規則

您能夠用不一樣的方式來組合上述的路由規則,例如:

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

route.handler(routingContext -> {

  // 這會匹配全部路徑以 `/myapi/orders` 開頭,`content-type` 值爲 `application/json` 而且 `accept` 值爲 `application/json` 的 PUT 請求

});

啓用和停用 Route

您能夠經過 disable 方法來停用一個 Route。停用的 Route 在匹配時會被忽略。

您能夠用 enable 方法來從新啓用它。

上下文數據

在請求的生命週期中,您能夠經過路由上下文 RoutingContext 來維護任何您但願在處理器之間共享的數據。

如下是一個例子,一個處理器設置了一些數據,另外一個處理器獲取它:

您可使用 put 方法向上下文設置任何對象,使用 get 方法從上下文中獲取任何對象。

一個路徑爲 /some/path/other 的請求會同時匹配兩個 Route:

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

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

});

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

  String bar = routingContext.get("foo");
  // 執行某些操做
  routingContext.response().end();

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

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

});

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

  String bar = routingContext.get("foo");
  // 執行某些操做
  routingContext.response().end();

});

另外一種您能夠訪問上下文數據的方式是使用 data 方法。

轉發

(4) 到目前爲止,經過上述的路由機制您能夠順序地處理您的請求,但某些狀況下您可能須要回退。因爲處理器的順序是動態的,路由上下文並無暴露出任何關於前一個或後一個處理器的信息。惟一的方式是在當前的 Router 裏重啓 Route的流程。

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();
  }
});

須要澄清的是,重定向是基於路徑的。也就是說,若是您須要在重定向的過程當中添加或者保持狀態,您須要使用 RoutingContext 對象。例如您但願使用一個新的參數重定向到另一個路徑:

router.get("/final-target").handler(ctx -> {
  // 繼續作某些事情
});

// 錯誤的方式! (會重定向到 /final-target 路徑,但不包含查詢參數)
router.get().handler(ctx -> {
  ctx.reroute("/final-target?variable=value");
});

// 正確的方式
router.get().handler(ctx -> {
  ctx
    .put("variable", "value")
    .reroute("/final-target");
});

雖然在重定向時會警告您查詢參數會丟失,可是重定向的過程仍然會執行。而且會從路徑上裁剪掉全部的查詢參數或 HTML 錨點。

子路由

當您有不少處理器的狀況下,合理的方式是將它們分隔爲多個 Router。這也有利於您在多個不用的應用中經過設置不一樣的根路徑來複用處理器。

您能夠經過將一個 Router 掛載到另外一個 Router 的掛載點上來實現。掛載的 Router 被稱爲子路由(Sub Router)。Sub router 上也能夠掛載其餘的 sub router。所以,您能夠包含若干級別的 sub router。

讓咱們看一個 sub router 掛載到另外一個 Router 上的例子:

這個 sub router 維護了一系列處理器,對應了一個虛構的 REST API。咱們會將它掛載到另外一個 Router上。 例子忽略了 REST API 的具體實現:

Router restAPI = Router.router(vertx);

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

  // TODO 查找產品信息
  rc.response().write(productJSON);

});

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

  // TODO 添加新的產品
  rc.response().end();

});

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

  // TODO 刪除產品
  rc.response().end();

});

若是這個 Router 是一個頂級的 Router,那麼例如 /products/product1234 這種 URL 的 GET/PUT/DELETE 請求都會調用這個 API。

若是咱們已經有了一個網站包含如下的 Router

Router mainRouter = Router.router(vertx);

// 處理靜態資源
mainRouter.route("/static/*").handler(myStaticHandler);

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

咱們能夠將這個 sub router 經過一個掛載點掛載到主 router 上,這個例子使用了 /preoductAPI

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

這意味着這個 REST API 如今能夠經過這種路徑訪問:/productsAPI/products/product1234

本地化

Vert.x Web 解析 Accept-Language 消息頭並提供了一些識別客戶端偏好的語言,以及提供經過 quality排序的語言偏好列表的方法。

Route route = router.get("/localized").handler( rc -> {
  //雖然經過一個 switch 循環有點奇怪,咱們必須按順序選擇正確的本地化方式
  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;
    }
  }
  // 咱們不知道用戶的語言,所以返回這個信息:
  rc.response().end("Sorry we don't speak: " + rc.preferredLocale());
});

方法 acceptableLocales 會返回客戶端可以理解的排序好的語言列表。 若是您只關心用戶偏好的語言,那麼使用 preferredLocale 會返回列表的第一個元素。 若是用戶沒有提供,則返回空。

默認的 404 處理器

若是沒有爲請求匹配到任何路由,Vert.x Web 會聲明一個 404 錯誤。

這能夠被您本身實現的處理器處理,或者被咱們提供的專用錯誤處理器(failureHandler)處理。 若是沒有提供錯誤處理器,Vert.x Web 會發送一個基本的 404 (Not Found) 響應。

錯誤處理

和設置處理器處理請求同樣,您能夠設置處理器處理路由過程當中的失敗。

失敗處理器和普通的處理器具備徹底同樣的路由匹配規則。

例如您能夠提供一個失敗處理器只處理在某個路徑上發生的失敗,或某個 HTTP 方法。

這容許您在應用的不一樣部分設置不一樣的失敗處理器。

下面例子中的失敗處理器只會在路由路徑爲 /somepath/ 的 GET 請求失敗時被調用:

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

route.failureHandler(frc -> {

  // 若是在處理路徑以 `/somepath/` 開頭的請求過程當中發生錯誤,會調用這個處理器

});

當一個處理器拋出異常,或者一個處理器經過了 fail 方法指定了 HTTP 狀態碼時,會執行路由的失敗處理。

從一個處理器捕捉到異常時會標記一個狀態碼爲 500 的錯誤。

在處理這個錯誤時,RoutingContext 會被傳遞到失敗處理器裏,失敗處理器能夠經過獲取到的錯誤或錯誤編碼來構造失敗的響應內容。

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

route1.handler(routingContext -> {

  // 這裏拋出一個 RuntimeException
  throw new RuntimeException("something happened!");

});

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

route2.handler(routingContext -> {
  // 這裏故意將請求處理爲失敗狀態
  // 例如 403 - 禁止訪問
  routingContext.fail(403);

});

// 定義一個失敗處理器,上述的處理器發生錯誤時會調用這個處理器
Route route3 = router.get("/somepath/*");

route3.failureHandler(failureRoutingContext -> {

  int statusCode = failureRoutingContext.statusCode();

  // 對於 RuntimeException 狀態碼會是 500,不然是 403
  HttpServerResponse response = failureRoutingContext.response();
  response.setStatusCode(statusCode).end("Sorry! Not today");

});

某些狀況下失敗處理器會因爲使用了不支持的字符集做爲狀態消息而致使錯誤。在這種狀況下,Vert.x Web 會將狀態消息替換爲狀態碼的默認消息。 這是爲了保證 HTTP 協議的語義,而不至於崩潰並斷開 socket 致使協議運行的不完整。

處理請求消息體

您可使用消息體處理器 BodyHandler 來獲取請求的消息體,限制消息體大小,或者處理文件上傳。

您須要保證消息體處理器可以匹配到全部您須要這個功能的請求。

因爲它須要在全部異步執行以前處理請求的消息體,所以這個處理器要儘量早地設置到 router 上。

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

獲取請求的消息體

若是您知道消息體的類型是 JSON,您可使用 getBodyAsJson;若是您知道它的類型是字符串,您可使用 getBodyAsString;不然能夠經過 getBody 做爲 Buffer 來處理。

限制消息體大小

若是要限制請求消息體的大小,能夠在建立消息體處理器時使用 setBodyLimit 來指定消息體的最大字節數。這對於規避因爲過大的消息體致使的內存溢出的問題頗有用。

若是嘗試發送一個大於最大值的消息體,則會獲得一個 HTTP 狀態碼 413 - Request Entity Too Large 的響應。

默認的沒有消息體大小限制。

合併表單屬性

消息體處理器默認地會合並表單屬性到請求的參數裏。 若是您不須要這個行爲,能夠經過 setMergeFormAttributes 來禁用。

處理文件上傳

消息體處理器也能夠用於處理 Multipart 的文件上傳。

當消息體處理器匹配到請求時,全部上傳的文件會被自動地寫入到上傳目錄中,默認的該目錄爲 file-uploads

每個上傳的文件會被自動生成一個文件名,並能夠經過 RoutingContext 的 fileUploads 來得到。

如下是一個例子:

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

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

  Set<FileUpload> uploads = routingContext.fileUploads();

  // 執行上傳處理

});

每個上傳的文件經過一個 FileUpload 對象來描述,經過這個對象能夠得到名稱、文件名、大小等屬性。

Vert.x Web 經過 Cookie 處理器 CookieHandler 來支持 cookie。

您須要保證 cookie 處理器器可以匹配到全部您須要這個功能的請求。

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

您可使用 getCookie 來經過名稱獲取 cookie 值,或者使用 cookies 獲取整個集合。

使用 removeCookie 來刪除 cookie。

使用 addCookie 來添加 cookie。

當向響應中寫入響應消息頭時,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();

  // 使用 cookie 執行某些操做

  // 添加一個 cookie,會自動回寫到響應裏
  routingContext.addCookie(Cookie.cookie("othercookie", "somevalue"));
});

處理會話

Vert.x Web 提供了開箱即用的會話(session)支持。

會話維持了 HTTP 請求和瀏覽器會話之間的關係,並提供了能夠設置會話範圍的信息的能力,例如一個購物籃。

Vert.x Web 使用會話 cookie(5) 來標示一個會話。會話 cookie 是臨時的,當瀏覽器關閉時會被刪除。

咱們不會在會話 cookie 中設置實際的會話數據,這個 cookie 只是在服務器上查找實際的會話數據時使用的標示。這個標示是一個經過安全的隨機過程生成的 UUID,所以它是沒法推測的(6)。

Cookie 會在 HTTP 請求和響應之間傳遞。所以經過 HTTPS 來使用會話功能是明智的。若是您嘗試直接經過 HTTP 使用會話,Vert.x Web 會給於警告。

您須要在匹配的 Route 上註冊會話處理器 SessionHandler 來啓用會話功能,並確保它可以在應用邏輯以前執行。

會話處理器會建立會話 Cookie 並查找會話信息,您不須要本身來實現。

會話存儲

您須要提供一個會話存儲對象來建立會話處理器。會話存儲用於維持會話數據。

會話存儲持有一個僞隨機數生成器(PRNG)用於安全地生成會話標示。PRNG 是獨立於存儲的,這意味着對於給定的存儲 A 的會話標示是不可以派發出存儲 B 的會話標示的,由於他們具備不一樣的種子和狀態。

PRNG 默認使用混合模式,阻塞式地刷新種子,非阻塞式地生成隨機數(7)。PRNG 會每隔 5 分鐘使用一個新的 64 位的熵做爲種子。這個策略能夠經過系統屬性來設置:

  • io.vertx.ext.auth.prng.algorithm e.g.: SHA1PRNG
  • io.vertx.ext.auth.prng.seed.interval e.g.: 1000 (every second)
  • io.vertx.ext.auth.prng.seed.bits e.g.: 128

大多數用戶並不須要配置這些值,除非您發現應用的性能被 PRNG 的算法所影響。

Vert.x Web 提供了兩種開箱即用的會話存儲實現,您也能夠編寫您本身的實現。

本地會話存儲

該存儲將會話保存在內存中,並只在當前實例中有效。

這個存儲適用於您只有一個 Vert.x 實例的狀況,或者您正在使用粘性會話。也就是說您能夠配置您的負載均衡器來確保全部請求(來自同一用戶的)永遠被派發到同一個 Vert.x 實例上。

若是您不可以保證這一點,那麼就不要使用這個存儲。這會致使請求被派發到沒法識別這個會話的服務器上。

本地會話存儲基於本地的共享 Map來實現,幷包含了一個用於清理過時會話的回收器。

回收的週期能夠經過 LocalSessionStore.create 來配置。

如下是一些建立 LocalSessionStore 的例子:

SessionStore store1 = LocalSessionStore.create(vertx);

// 經過指定的 Map 名稱建立了一個本地會話存儲
// 這適用於您在同一個 Vert.x 實例中有多個應用,而且但願不一樣的應用使用不一樣的 Map 的狀況
SessionStore store2 = LocalSessionStore.create(vertx, "myapp3.sessionmap");

// 經過指定的 Map 名稱建立了一個本地會話存儲
// 設置了檢查過時 Session 的週期爲 10 秒
SessionStore store3 = LocalSessionStore.create(vertx, "myapp3.sessionmap", 10000);

集羣會話存儲

該存儲將會話保存在分佈式 Map 中,該 Map 能夠在 Vert.x 集羣中共享訪問。

這個存儲適用於您沒有使用粘性會話的狀況。好比您的負載均衡器會未來自同一個瀏覽器的不一樣請求轉發到不一樣的服務器上。

經過這個存儲,您的會話能夠被集羣中的任何節點訪問。

若是要使用集羣會話存儲,您須要確保您的 Vert.x 實例是集羣模式的。

如下是一些建立 ClusteredSessionStore 的例子:

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

  Vertx vertx = res.result();

  // 建立了一個默認的集羣會話存儲
  SessionStore store1 = ClusteredSessionStore.create(vertx);

  // 經過指定的 Map 名稱建立了一個集羣會話存儲
  // 這適用於您在集羣中有多個應用,而且但願不一樣的應用使用不一樣的 Map 的狀況
  SessionStore store2 = ClusteredSessionStore.create(vertx, "myclusteredapp3.sessionmap");
});

建立會話處理器

當您建立會話存儲以後,您能夠建立一個會話處理器,並添加到 Route 上。您須要確保會話處理器在您的應用處理器以前被執行。

因爲會話處理器須要使用 Cookie 來查找會話,所以您還須要包含一個 Cookie 處理器。這個 Cookie 處理器須要在會話處理器以前被執行。

如下是例子:

Router router = Router.router(vertx);

// 咱們首先須要一個 cookie 處理器
router.route().handler(CookieHandler.create());

// 用默認值建立一個集羣會話存儲
SessionStore store = ClusteredSessionStore.create(vertx);

SessionHandler sessionHandler = SessionHandler.create(store);

// 確保全部請求都會通過 session 處理器
router.route().handler(sessionHandler);

// 您本身的應用處理器
router.route("/somepath/blah/").handler(routingContext -> {

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

});

會話處理器會自動從會話存儲中查找會話(若是沒有則建立),並在您的應用處理器執行以前設置在上下文中。

使用會話

在您的處理器中,您能夠經過 session 方法來訪問會話對象。

您能夠經過 put 方法來向會話中設置數據,經過 get 方法來獲取數據,經過 remove 方法來刪除數據。

會話中的鍵的類型必須是字符串。本地會話存儲的值能夠是任何類型;集羣會話存儲的值類型能夠是基本類型,或者 BufferJsonObjectJsonArray 或可序列化對象。由於這些值須要在集羣中進行序列化。

如下是操做會話數據的例子:

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

// 您的應用處理器
router.route("/somepath/blah").handler(routingContext -> {

  Session session = routingContext.session();

  // 向會話中設置值
  session.put("foo", "bar");

  // 從會話中獲取值
  int age = session.get("age");

  // 從會話中刪除值
  JsonObject obj = session.remove("myobj");

});

在響應完成後會話會自動回寫到存儲中。

您可使用 destroy 方法來銷燬一個會話。這會將這個會話同時從上下文和存儲中刪除。注意,在刪除會話以後,下一次經過瀏覽器訪問並通過會話處理器處理時,會自動建立新的會話。

會話超時

若是會話在指定的週期內沒有被訪問,則會超時。

當請求到達,訪問了會話,而且在響應完成向會話存儲回寫會話時,會話會被標記爲被訪問的。

您也能夠經過 setAccessed 來人工指定會話被訪問。

能夠在建立會話處理器時配置超時時間。默認的超時時間是 30 分鐘。

認證/受權

Vert.x Web 提供了若干開箱即用的處理器來處理認證和受權。

建立認證處理器

您須要一個 AuthProvider 實例來建立認證處理器。Auth Provider 用於爲用戶提供認證和受權。Vert.x 在 vertx-auth 項目中提供了若干開箱即用的 Auth Provider。完整的 Auth Provider 的配置和用法請參考 Vertx Auth 的文檔

如下是一個使用 Auth Provider 來建立認證處理器的例子:

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

AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider);

在您的應用中處理認證

咱們來假設您但願全部路徑爲 /private 的請求都須要認證控制。爲了實現這個,您須要確保您的認證處理器匹配這個路徑,並在您的應用處理器以前執行:

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

AuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider);

// 全部路徑以 `/private` 開頭的請求會被保護
router.route("/private/*").handler(basicAuthHandler);

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

  // 此處是公開的,不須要登陸

});

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

  // 此處須要登陸

  // 這個值會返回 true
  boolean isAuthenticated = routingContext.user() != null;

});

若是認證處理器完成了受權和認證,它會向 RoutingContext 中注入一個 User 對象。您能夠經過 user方法在您的處理器中獲取到該對象。

若是您但願在回話中存儲用戶對象,以免對全部的請求都執行認證過程,您須要使用會話處理器。確保它匹配了對應的路徑,而且會在認證處理器以前執行。

一旦您獲取到了 user 對象,您能夠經過編程的方式來使用它的相關方法爲用戶受權。

若是您但願用戶登出,您能夠調用上下文的 clearUser 方法。

HTTP 基礎認證

HTTP基礎認證是適用於簡單應用的簡單認證手段。

在這種認證方式下, 證書會以非加密的形式在 HTTP 請求中傳輸。所以,使用 HTTPS 而非 HTTP 來實現您的應用是很是必要的。

當用戶請求一個須要受權的資源,基礎認證處理器會返回一個包含 WWW-Authenticate 消息頭的 401 響應。瀏覽器會顯示一個登陸窗口並提示用戶輸入他們的用戶名和密碼。

在這以後,瀏覽器會從新發送這個請求,並將用戶名和密碼以 Base64 編碼的形式包含在請求的Authorization 消息頭裏。

當基礎認證處理器收到了這些信息,它會使用用戶名和密碼調用配置的 AuthProvider 來認證用戶。若是認證成功則該處理器會嘗試用戶受權,若是也成功了則容許這個請求路由到後續的處理器裏處理。不然,會返回一個 403 的響應拒絕訪問。

在設置認證處理器時能夠指定一系列訪問資源時須要的權限。

重定向認證處理器

重定向認證處理器用於當未登陸的用戶嘗試訪問受保護的資源時將他們重定向到登陸頁上。

當用戶提交登陸表單,服務器會處理用戶認證。若是成功,則將用戶重定向到原始的資源上。

則您能夠配置一個 RedirectAuthHandler 對象來使用重定向處理器。

您還須要配置用於處理登陸頁面的處理器,以及實際處理登陸的處理器。咱們提供了一個內置的處理器 FormLoginHandler 來處理登陸的問題。

這裏是一個簡單的例子,使用了一個重定向認證處理器並使用默認的重定向 url /loginpage

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

AuthHandler redirectAuthHandler = RedirectAuthHandler.create(authProvider);

// 全部路徑以 `/private` 開頭的請求會被保護
router.route("/private/*").handler(redirectAuthHandler);

// 處理登陸請求
// 您的登陸頁須要 POST 登陸表單數據
router.post("/login").handler(FormLoginHandler.create(authProvider));

// 處理靜態資源,例如您的登陸頁
router.route().handler(StaticHandler.create());

router.route("/someotherpath").handler(routingContext -> {
  // 此處是公開的,不須要登陸
});

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

  // 此處須要登陸

  // 這個值會返回 true
  boolean isAuthenticated = routingContext.user() != null;

});

JWT 受權

JWT 受權經過權限來保護資源不被未爲受權的用戶訪問。

使用這個處理器涉及 2 個步驟:

  • 配置一個處理器用於頒發令牌(或依靠第三方)
  • 配置受權處理器來過濾請求

注意,這兩個處理器應該只能經過 HTTPS 訪問。不然可能會引發由流量嗅探引發的會話劫持。

這裏是一個派發令牌的例子:

Router router = Router.router(vertx);

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

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

router.route("/login").handler(ctx -> {
  // 這是一個例子,認證會由另外一個 provider 執行
  if ("paulo".equals(ctx.request().getParam("username")) && "secret".equals(ctx.request().getParam("password"))) {
    ctx.response().end(authProvider.generateToken(new JsonObject().put("sub", "paulo"), new JWTOptions()));
  } else {
    ctx.fail(401);
  }
});

注意,對於持有令牌的客戶端,惟一須要作的是在 全部 後續的的 HTTP 請求中包含消息頭 Authorization並寫入 Bearer <token>,例如:

Router router = Router.router(vertx);

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

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

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

router.route("/protected/somepage").handler(ctx -> {
  // 一些處理過程
});

JWT 容許您向令牌中添加任何您須要的信息,只須要在建立令牌時向 JsonObject 參數中添加數據便可。這樣作服務器上不存在任何的會話狀態,您能夠在不依賴集羣會話數據的狀況下對應用進行擴展。

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

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

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

在消費時用一樣的方式:

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

配置所需的權限

您能夠對認證處理器配置訪問資源所需的權限。

默認的,若是不配置權限,那麼只要登陸了就能夠訪問資源。不然,用戶不只須要登陸,並且須要具備所需的權限。

如下的例子定義了一個應用,該應用的不一樣部分須要不一樣的權限。注意,權限的含義取決於您使用的的 Auth Provider。例如一些支持角色/權限的模型,另外一些多是其餘的模型。

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

// 須要 `list_products` 權限來列舉產品
router.route("/listproducts/*").handler(listProductsAuthHandler);

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

// 只有 `admin` 能夠訪問 `/private/settings`
router.route("/private/settings/*").handler(settingsAuthHandler);

靜態資源服務

Vert.x Web 提供了一個開箱即用的處理器來提供靜態的 Web 資源。您能夠很是容易地編寫靜態的 Web 服務器。

您可使用靜態資源處理器 StaticHandler 來提供諸如 .html.css.js 或其餘類型的靜態資源。

每個被靜態資源處理器處理的請求都會返回文件系統的某個目錄或 classpath 裏的文件。文件的根目錄是能夠配置的,默認爲 webroot

在如下的例子中,全部路徑以 /static 開頭的請求都會對應到 webroot 目錄:

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

例如,對於一個路徑爲 /static/css/mystyles.css 的請求,靜態處理器會在該路徑中查找文件 webroot/css/mystyle.css

它也會在 classpath 中查找文件 webroot/css/mystyle.css。這意味着您能夠將全部的靜態資源打包到一個 jar 文件(或 fat-jar)裏進行分發。

當 Vert.x 在 classpath 中第一次找到一個資源時,會將它提取到一個磁盤的緩存目錄中以免每一次都從新提取。

這個處理器可以處理範圍請求。當客戶端請求靜態資源時,該處理器會添加一個範圍單位的說明到響應的消息頭 Accept-Ranges 裏來通知客戶端它支持範圍請求。若是後續請求的消息頭 Range 裏包含了正確的單位以及起始、終止位置,則客戶端將收到包含了的 Content-Range 消息頭的部分響應。

配置緩存

默認的,爲了讓瀏覽器有效地緩存文件,靜態處理器會設置緩存消息頭。

Vert.x Web 會在響應裏設置這些消息頭:cache-controllast-modifieddate

cache-control 的默認值爲 max-age=86400,也就是一天。能夠經過 setMaxAgeSeconds 方法來配置。

當瀏覽器發送了攜帶消息頭 if-modified-since 的 GET 或 HEAD 請求時,若是對應的資源在該日期以後沒有修改過,則會返回一個 304 狀態碼通知瀏覽器使用本地的緩存資源。

若是不須要緩存的消息頭,能夠經過 setCachingEnabled 方法將其禁用。

若是啓用了緩存處理,則 Vert.x Web 會將資源的最後修改日期緩存在內存裏,以此來避免頻繁地訪問取磁盤來檢查修改時間。

緩存有過時時間,在這個時間以後,會從新訪問磁盤檢查文件並更新緩存。

默認的,若是您的文件永遠不會發生變化,則緩存內容會永遠有效。

若是您的文件在服務器運行過程當中可能發生變化,您能夠經過 setFilesReadOnly 方法設置文件的只讀屬性爲 false。

您能夠經過 setMaxCacheSize 方法來設置內存緩存的最大數量。經過 setCacheEntryTimeout 方法來設置緩存的過時時間。

配置索引頁

全部訪問根路徑 / 的請求會被定位到索引頁。默認的該文件爲 index.html。能夠經過 setIndexPage 方法來設置。

配置跟目錄

默認的,全部資源都以 webroot 做爲根目錄。能夠經過 setWebRoot 方法來配置。

隱藏文件

默認的,處理器會爲隱藏文件提供服務(文件名以 . 開頭的文件)。

若是您不須要爲隱藏文件提供服務,能夠經過 setIncludeHidden 方法來配置。

列舉目錄

靜態資源處理器能夠用於列舉目錄的文件。默認狀況下該功能是關閉的。能夠經過 setDirectoryListing 方法來啓用。

當該功能啓用時,會根據客戶端請求的消息頭 accept 所表示的類型來返回相應的結果。

例如對於 text/html 標示的請求,會使用經過 setDirectoryTemplate 方法設置的模板來渲染文件列表。

禁用磁盤文件緩存

默認狀況下,Vert.x 會使用當前工做目錄的子目錄 .vertx 來在磁盤上緩存經過 classpath 服務的靜態資源。這對於在生產環境中經過 fat-jar 來部署的服務是很重要的。由於每一次都經過 classpath 來提取文件是低效的。

這在開發時會致使一個問題,當您在服務運行過程當中修改了靜態內容,緩存的文件是不會被更新的。

您能夠設置 vert.x 的 fileResolverCachingEnabled 選項爲 true 來禁用文件緩存。爲了向後兼容,它會從 vertx.disableFileCaching 這個系統屬性裏來提取默認值。例如,您若是從 IDE 來啓動您的應用程序,能夠在 IDE 的運行配置中來配置這個屬性。

處理跨域資源共享

跨域資源共享(CORS,Cross Origin Resource Sharing)是一個安全機制。該機制容許了瀏覽器在一個域名下訪問另外一個域名的資源。

Vert.x Web 提供了一個處理器 CorsHandler 來爲您處理 CORS 協議。

這是一個例子:

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

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

  // 您的應用處理

});

模板引擎

Vert.x Web 爲若干流行的模板引擎提供了開箱即用的支持,經過這種方式來提供生成動態頁面的能力。您也能夠很容易地添加您本身的實現。

模板引擎 TemplateEngine 定義了使用模板引擎的接口,當渲染模板時會調用 render 方法。

最簡單的使用模板的方式不是直接調用模板引擎,而是使用模板處理器 TemplateHandler。這個處理器會根據 HTTP 請求的路徑來調用模板引擎。

默認的,模板處理器會在 templates 目錄中查找模板文件。這是能夠配置的。

該處理器會返回渲染的結果,並默認設置 Content-Type 消息頭爲 text/html。這也是能夠配置的。

您須要在建立模板處理器時提供您須要使用的模板引擎的實例。

模板引擎的實現沒有內嵌在 Vert.x Web 裏,您須要配置您的項目來訪問它們。Vert.x Web 提供了每一種模板引擎的配置。

如下是一個例子:

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

// 這會將全部以 `/dynamic` 開頭的請求路由到模板處理器上
// 例如 /dynamic/graph.hbs 會查找模板 /templates/graph.hbs
router.get("/dynamic/*").handler(handler);

// 將全部以 `.hbs` 結尾的請求路由到模板處理器上
router.getWithRegex(".+\\.hbs").handler(handler);

MVEL 模板引擎

您須要在您的項目中添加這些依賴來使用 MVEL 模板引擎:io.vertx:vertx-web-templ-mvel:3.4.2。經過這個方法來建立 MVEL 模板引擎的實例:io.vertx.ext.web.templ.MVELTemplateEngine#create()

在使用 MVEL 模板引擎時,若是不指定模板文件的擴展名,則默認會查找擴展名爲 .templ 的文件。

在 MVEL 模板中能夠經過 context 變量來訪問路由上下文 RoutingContext 對象。這也意味着您能夠基於上下文裏的任何信息來渲染模板,包括請求、響應、會話或者上下文數據。

這是一個例子:

The request path is @{context.request().path()}

The variable 'foo' from the session is @{context.session().get('foo')}

The value 'bar' from the context data is @{context.get('bar')}

關於如何編寫 MVEL 模板,請參考 MVEL 模板文檔

Jade 模板引擎

譯者注:Jade 已改名爲 Pug。

您須要在您的項目中添加這些依賴來使用 Jade 模板引擎:io.vertx:vertx-web-templ-jade:3.4.2。經過這個方法來建立 Jade 模板引擎的實例:io.vertx.ext.web.templ.JadeTemplateEngine#create()

在使用 Jade 模板引擎時,若是不指定模板文件的擴展名,則默認會查找擴展名爲 .jade 的文件。

在 Jade 模板中能夠經過 context 變量來訪問路由上下文 RoutingContext 對象。這也意味着您能夠基於上下文裏的任何信息來渲染模板,包括請求、響應、會話或者上下文數據。

這是一個例子:

!!! 5
html
  head
    title= context.get('foo') + context.request().path()
  body

關於如何編寫 Jade 模板,請參考 Jade4j 文檔

Handlebars 模板引擎

您須要在您的項目中添加這些依賴來使用 Handlebars:io.vertx:vertx-web-templ-handlebars:3.4.2。經過這個方法來建立 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 模板,請參考 Handlebars Java 文檔

Thymeleaf 模板引擎

您須要在您的項目中添加這些依賴來使用 Thymeleaf:io.vertx:vertx-web-templ-thymeleaf:3.4.2。經過這個方法來建立 Thymeleaf 模板引擎的實例:

io.vertx.ext.web.templ.ThymeleafTemplateEngine#create()。

在使用 Thymeleaf 模板引擎時,若是不指定模板文件的擴展名,則默認會查找擴展名爲 .html 的文件。

在 Thymeleaf 模板中能夠經過 context 變量來訪問路由上下文 RoutingContext 對象。這也意味着您能夠基於上下文裏的任何信息來渲染模板,包括請求、響應、會話或者上下文數據。

這是一個例子:

[snip]
<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>
[snip]

關於如何編寫 Thymeleaf 模板,請參考 Thymeleaf 文檔

Apache FreeMarker 模板引擎

您須要在您的項目中添加這些依賴來使用 Apache FreeMarker:io.vertx:vertx-web-templ-freemarker:3.4.2。經過這個方法來建立 Apache FreeMarker 模板引擎的實例:io.vertx.ext.web.templ.FreeMarkerTemplateEngine#create()

在使用 Apache FreeMarker 模板引擎時,若是不指定模板文件的擴展名,則默認會查找擴展名爲 .ftl 的文件。

在 Apache FreeMarker 模板中能夠經過 context 變量來訪問路由上下文 RoutingContext 對象。這也意味着您能夠基於上下文裏的任何信息來渲染模板,包括請求、響應、會話或者上下文數據。

這是一個例子:

[snip]
<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>
[snip]

關於如何編寫 Apache FreeMarker 模板,請參考 Apache FreeMarker 文檔

Pebble 模板引擎

您須要在您的項目中添加這些依賴來使用 Pebble:io.vertx:vertx-web-templ-pebble:3.4.0-SNAPSHOT。經過這個方法來建立 Pebble 模板引擎的實例:io.vertx.ext.web.templ.PebbleTemplateEngine#create()

在使用 Pebble 模板引擎時,若是不指定模板文件的擴展名,則默認會查找擴展名爲 .peb 的文件。

在 Pebble 模板中能夠經過 context 變量來訪問路由上下文 RoutingContext 對象。這也意味着您能夠基於上下文裏的任何信息來渲染模板,包括請求、響應、會話或者上下文數據。

這是一個例子:

[snip]
<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>
[snip]

關於如何編寫 Pebble 模板,請參考 Pebble 文檔

禁用緩存

在開發時,爲了讓每一次請求能夠從新讀取模板內容,您可能但願禁用模板的緩存。這能夠經過設置系統屬性 io.vertx.ext.web.TemplateEngine.disableCache 爲 true 來實現。

默認的這個值爲 false,也就是開啓模板緩存。

錯誤處理

您能夠用模板處理器來渲染錯誤信息,或者使用 Vert.x Web 內置的一個 」漂亮「 的、開箱即用的錯誤處理器來渲染錯誤頁面。

這個處理器是 ErrorHandler。您只須要在須要覆蓋到的路徑上將它設置爲失敗處理器(9)來使用它。

請求日誌

Vert.x Web 提供了一個用於記錄 HTTP 請求的處理器 LoggerHandler

默認的,請求會經過 Vert.x 日誌來記錄,或者也能夠配置爲 jul 日誌、log4j 或 slf4j。詳見 LoggerFormat

提供網頁圖標

Vert.x Web 經過內置的處理器 FaviconHandler 來提供網頁圖標。

圖標能夠指定爲文件系統上的某個路徑,不然 Vert.x Web 默認會在 classpath 上尋找 favicon.ico 文件。這意味着您能夠將圖標打包到您的應用的 jar 包裏。

超時處理器

Vert.x Web 提供了一個超時處理器,能夠在處理時間過長時將請求超時。

經過 TimeoutHandler 對象來進行配置。

若是一個請求在響應以前超時,則會給客戶端返回一個 503 的響應。

下面的例子設置了一個超時處理器。對於全部以 /foo 路徑開頭的請求,都會在執行 5 秒後自動超時。

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

響應時間處理器

該處理器會將從接收到請求到寫入響應的消息頭之間的毫秒數寫入到響應的 x-response-time 裏,例如:

x-response-time: 1456ms

Content Type 處理器

該處理器 ResponseContentTypeHandler 會自動設置響應的 Content-Type 消息頭。假設咱們要構建一個 RESTful 的 Web 應用,咱們須要在全部處理器裏設置 Content-Type

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 接口數量的增加,設置 Content-Type 會變得很麻煩。能夠經過在 Route 上添加 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 方法來選擇適當的 Content-Type。所以,您能夠很容易地使用同一個處理器來提供不一樣類型的數據:

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

SockJS

SockJS 是一個客戶端的 JavaScript 庫。它提供了相似 WebSocket 的接口爲您和 SockJS 服務端創建鏈接。您沒必要關注瀏覽器或網絡是否真的是 WebSocket。

它提供了若干不一樣的傳輸方式,並在運行時根據瀏覽器和網絡的兼容性來選擇使用哪一種傳輸方式處理。

全部這些對您是透明的,您只須要簡單地使用相似 WebSocket 的接口。

請訪問 SockJS 官方網站 來獲取 SockJS 的詳細信息。

SockJS 處理器

Vert.x Web 提供了一個開箱即用的處理器 SockJSHandler 來讓您在 Vert.x Web 應用中使用 SockJS。

您須要經過 SockJSHandler.create 方法爲每個 SockJS 的應用建立這個處理器。您也能夠在建立處理器時經過 SockJSHandlerOptions 對象來指定配置選項。

Router router = Router.router(vertx);

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

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

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

處理 SockJS 套接字

您能夠在服務器端設置一個處理器,這個處理器會在每次客戶端建立鏈接時被調用:

調用這個處理器的參數是一個 SockJSSocket 對象。這是一個相似套接字的接口,您能夠向使用 NetSocket 和 WebSocket 那樣經過它來讀寫數據。它實現了 ReadStream 和 WriteStream 接口,所以您能夠將它套用(pump)到其餘的讀寫流上。

下面的例子中的 SockJS 處理器直接使用了它讀取到的數據進行回寫:

Router router = Router.router(vertx);

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

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

sockJSHandler.socketHandler(sockJSSocket -> {

  // 將數據回寫
  sockJSSocket.handler(sockJSSocket::write);
});

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

SockJS 客戶端

在客戶端 JavaScript 環境裏您須要經過 SockJS 的客戶端庫來創建鏈接。

SockJS 客戶端的地址

完整的細節能夠在 SockJS 的網站 中找到,簡單來講您會像這樣使用:

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

sock.onopen = function() {
  console.log('open');
};

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

sock.onclose = function() {
  console.log('close');
};

sock.send('test');

sock.close();

配置 SockJS 處理器

能夠經過 SockJSHandlerOptions 對象來配置這個處理器的若干選項。

  • insertJSESSIONID

在 cookie 中插入一個 JSESSIONID,這樣負載均衡器能夠保證 SockJS 會話永遠轉發到正確的服務器上。默認值爲 true

  • sessionTimeout

對於一個正在接收響應的客戶端鏈接,若是一段時間內沒有動做,則服務端會發出一個 close 事件。延時時間由這個配置決定。默認的服務端會在 5 秒以後發出這個 close 事件。(10)

  • heartbeatInterval

咱們會每隔一段時間發送一個心跳包,用來避免因爲請求時間過長致使鏈接被代理和負載均衡器關閉。默認的每隔 25 秒發送一個心跳包,能夠經過這個設置來控制頻率。

  • maxBytesStreaming

大多數流式傳輸方式會在客戶端保存響應的內容而且不會釋放派發消息所使用的內存。這些傳輸方式須要按期執行垃圾回收。max_bytes_streaming 設置了每個 http 流式請求所須要發送的最小字節數。超過這個值則客戶端須要打開一個新的請求。將這個值設置得太小會失去流式的處理能力,使這個流式的傳輸方式表現得像一個輪訓的傳輸方式同樣。默認值是 128K。

  • libraryURL

對於沒有提供原生的跨域通訊支持的瀏覽器,會使用 iframe 來進行通訊。SockJS 服務器會提供一個簡單的頁面(在目標域名上)並放置在一個不可見的 iframe 裏。在 iframe 裏運行的代碼和 SockJS 服務器運行在同一個域名下,所以不用擔憂跨域的問題。這個 iframe 也須要加載 SockJS 的客戶端 JavaScript 庫,這個配置就是用於指定這個 URL 的。默認狀況下會使用最新發布的壓縮版本 http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js

  • disabledTransports

這個參數用於禁用某些傳輸方式。可能的值包括 WEBSOCKET、EVENT_SOURCE、HTML_FILE、JSON_P、XHR。

SockJS 橋接 Event Bus

Vert.x Web 提供了一個內置的叫作 Event Bus Bridge 的 SockJS 套接字處理器。該處理器用於將服務器端的 Vert.x 的 Event Bus 擴展到客戶端的 JavaScript 運行環境裏。

這將建立一個分佈式的 Event Bus。這個 Event Bus 不只能夠在多個 Vert.x 實例中使用,還能夠經過運行在瀏覽器裏的 JavaScript 訪問。

由此,咱們能夠圍繞瀏覽器和服務器構建一個龐大的分佈式 Event Bus。只要服務器之間的連接存在,瀏覽器不須要每一次都與同一個服務器創建連接。

這些是經過 Vert.x 提供的一個簡單的客戶端 JavaScript 庫 vertx-eventbus.js 來實現的。它提供了一系列和服務器端的 Vert.x Event Bus 相似的 API。經過這些 API 能夠發送或發佈消息,或註冊處理器來接收消息。

一個 SockJS 套接字處理器會被安裝到 SockJSHandler 上。這個處理器用於處理 SockJS 的數據並把它橋接到服務器端的 event bus 上。

Router router = Router.router(vertx);

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

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

在客戶端經過使用 vertx-eventbus.js 庫來和 Event Bus 創建鏈接,併發送/接收消息:

<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() {

  // 設置了一個接收數據的處理器
  eb.registerHandler('some-address', function(error, message) {
    console.log('received a message: ' + JSON.stringify(message));
  });

  // 發送消息
  eb.send('some-address', {name: 'tim', age: 587});

}

</script>

這個例子作的第一件事是建立了一個 Event Bus 實例:

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

構造函數中的參數是鏈接到 Event Bus 使用的 URI。因爲咱們建立的橋接器是以 eventbus 做爲前綴的,所以咱們須要將 URI 指向這裏。

在鏈接打開以前,咱們什麼也作不了。當它打開後,會回調 onopen 函數處理。

注意,不管是 SockJS 或是 EventBusBridge 都不支持自動重連

當你的服務器關閉時,你須要從新建立一個 EventBus 實例:

function setupEventBus() {
  var eb = new EventBus();
  eb.onclose = function (e) {
    setTimeout(setupEventBus, 1000); //等待服務器重啓
  };
  // 在這裏設置處理器
}

您能夠經過依賴管理器來獲取客戶端庫:

  • Maven (在您的 pom.xml 文件裏)
<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-web</artifactId>
  <version>3.4.2</version>
  <classifier>client</classifier>
  <type>js</type>
</dependency>
  • Gradle(在您的 build.gradle 文件裏)
compile 'io.vertx:vertx-web:3.4.2:client'

這個庫也能夠經過如下方式來獲取:

注意, 這個 API 在 3.0.0 和 3.1.0 版本之間發生了變化,請檢查變動日誌。老版本的客戶端仍然兼容,但新版本提供了更多的特性,而且更接近服務端的 Vert.x Event Bus API。

安全的橋接

若是您像上面的例子同樣啓動一個橋接器,並試圖發送消息,您會發現您的消息神祕地失蹤了。發生了什麼?

對於大多數的應用,您應該不但願客戶端的 JavaScript 代碼能夠發送任何消息到任何的服務端處理器或其餘全部瀏覽器上。

例如,您可能在Event Bus 上註冊了一個服務,用於訪問和刪除數據。但咱們並不但願惡意的客戶端可以經過這個服務來操做數據庫中的數據。而且,咱們也不但願客戶端可以監聽全部 event bus 上的地址。

爲了解決這個問題,SockJS 默認的會拒絕全部的消息。您須要告訴橋接器哪些消息是能夠經過的。(例外狀況是,全部的回覆消息都是能夠經過的)。

換句話說,橋接器的行爲像是配置了 deny-all 策略的防火牆。

爲橋接器配置哪些消息容許經過是很容易的。

您能夠經過調用橋接器時傳入的 BridgeOptions 來配置匹配規則,指定哪些輸入和輸出的流量是容許經過的。

每個匹配規則對應一個 PermittedOptions 對象:

setAddress

這個配置精確地定義了消息能夠被髮送到哪些地址。若是您須要經過精確的地址來控制消息的話,使用這個選項。

setAddressRegex

這個配置經過正則表達式來定義消息能夠被髮送到哪些地址。若是您須要經過正則表達式來控制消息的話,使用這個選項。若是指定了 address,這個選項會被忽略。

setMatch

這個配置經過消息的接口來控制消息是否能夠發送。這個配置中定義的每個字段必須在消息中存在,而且值一致。這個配置只能用於 JSON 格式的消息。

對於一個輸入的消息(例如經過客戶端 JavaScript 發送到服務器),當消息到達時,Vert.x Web 會檢查每一條輸入許可。若是存在匹配,則消息能夠經過。

對於一個輸出的消息(例如經過服務器端發送給客戶端 JavaScript),當消息發送時,Vert.x Web 會檢查每一條輸出許可。若是存在匹配,則消息能夠經過。

實際的匹配過程以下:

若是指定了 address 字段,而且消息的目標地址與 address 精確匹配,則匹配成功。

若是沒有指定 address 而是指定了 addressRegex 字段,而且消息的目標地址匹配了這個正則表達式,則匹配成功。

若是指定了 match 字段,而且消息中包含了 match 對象中的全部鍵值對,則匹配成功。

如下是例子:

Router router = Router.router(vertx);

SockJSHandler sockJSHandler = SockJSHandler.create(vertx);


// 容許客戶端向地址 `demo.orderMgr` 發送消息
PermittedOptions inboundPermitted1 = new PermittedOptions().setAddress("demo.orderMgr");

// 容許客戶端向地址 `demo.orderMgr` 發送消息
// 而且 `action` 的值爲 `find`、`collecton` 的值爲 `albums` 消息。
PermittedOptions inboundPermitted2 = new PermittedOptions().setAddress("demo.persistor")
    .setMatch(new JsonObject().put("action", "find")
        .put("collection", "albums"));

// 容許 `wibble` 值爲 `foo` 的消息.
PermittedOptions inboundPermitted3 = new PermittedOptions().setMatch(new JsonObject().put("wibble", "foo"));

// 下面定義瞭如何容許服務端向客戶端發送消息

// 容許向客戶端發送地址爲 `ticker.mystock` 的消息
PermittedOptions outboundPermitted1 = new PermittedOptions().setAddress("ticker.mystock");

// 容許向客戶端發送地址以 `news.` 開頭的消息(例如 news.europe, news.usa, 等)
PermittedOptions outboundPermitted2 = new PermittedOptions().setAddressRegex("news\\..+");

// 將規則添加到 BridgeOptions 裏
BridgeOptions options = new BridgeOptions().
    addInboundPermitted(inboundPermitted1).
    addInboundPermitted(inboundPermitted1).
    addInboundPermitted(inboundPermitted3).
    addOutboundPermitted(outboundPermitted1).
    addOutboundPermitted(outboundPermitted2);

sockJSHandler.bridge(options);

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

消息受權

Event Bus 橋接器可使用 Vert.x Web 的受權功能來配置消息的訪問受權。同時支持輸入和輸出。

這能夠經過向上文所述的匹配規則中加入額外的字段來指定該匹配須要哪些權限。

經過 setRequiredAuthority 方法來指定對於一個登陸用戶,須要具備哪些權限才容許訪問這個消息。

這是一個例子:

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

// 僅當用戶已登陸而且擁有權限 `place_orders`
inboundPermitted.setRequiredAuthority("place_orders");

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

用戶須要登陸,並被受權纔可以訪問消息。所以,您須要配置一個 Vert.x 認證處理器來處理登陸和受權。例如:

Router router = Router.router(vertx);

// 容許客戶端向地址 `demo.orderService` 發送消息
PermittedOptions inboundPermitted = new PermittedOptions().setAddress("demo.orderService");

// 僅當用戶已經登陸而且包含 `place_orders` 權限
inboundPermitted.setRequiredAuthority("place_orders");

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

// 設置基礎認證處理器

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);

處理 Event Bus 橋接器事件

若是您須要在在橋接器發生事件的時候獲得通知,您須要在調用 bridge 方法時提供一個處理器。

任何發生的事件都會被傳遞到這個處理器。事件由對象 BridgeEvent 來描述。

事件多是如下的某一種類型:

  • SOCKET_CREATED

    當新的 SockJS 套接字建立時會發生該事件。

  • SOCKET_IDLE

    當 SockJS 的套接字的空閒事件超過出事設置會發生該事件。

  • SOCKET_PING

    當 SockJS 的套接字的 ping 時間戳被更新時會發生該事件。

  • SOCKET_CLOSED

    當 SockJS 的套接字關閉時會發生該事件。

  • SEND

    當試圖將一個客戶端消息發送到服務端時會發生該事件。

  • PUBLISH

    當試圖將一個客戶端消息發佈到服務端時會發生該事件。

  • RECEIVE

    當試圖將一個服務器端消息發佈到客戶端時會發生該事件。

  • REGISTER

    當客戶端試圖註冊一個處理器時會發生該事件。

  • UNREGISTER

    當客戶端試圖註銷一個處理器時會發生該事件。

您能夠經過 type 方法來得到事件的類型,經過 getRawMessage 方法來得到消息原始內容。

消息的原始內容是一個以下結構的 JSON 對象:

{
  "type": "send"|"publish"|"receive"|"register"|"unregister",
  "address": the event bus address being sent/published/registered/unregistered
  "body": the body of the message
}

事件對象同時是一個 Future 實例。當您完成了對消息的處理,您能夠用參數 true 來完成這個 Future以執行後續的處理。

若是您不但願事件繼續處理,您能夠用參數 false 來結束這個 Future。這個特性能夠用於定製您本身的消息過濾器、細粒度的受權或指標收集。

在下面的例子裏,咱們拒絕掉了全部通過橋接器而且包含 「Armadillos」 一詞的消息:

Router router = Router.router(vertx);

// 容許客戶端向地址 `demo.orderMgr` 發送消息
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")) {
      // 拒絕該消息
      be.complete(false);
      return;
    }
  }
  be.complete(true);
});

router.route("/eventbus").handler(sockJSHandler);
Router router = Router.router(vertx);

// 容許客戶端向地址 `demo.orderMgr` 發送消息
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")) {
      // 拒絕該消息
      be.complete(false);
      return;
    }
  }
  be.complete(true);
});

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

下面的例子展現瞭如何配置並處理 SOCKET_IDDLE 事件。注意,setPingTimeout(5000) 的做用是當 ping 消息在 5 秒內沒有從客戶端返回時觸發 SOCKET_IDLE 事件。

Router router = Router.router(vertx);

// 初始化 SockJS 處理器
SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
BridgeOptions options = new BridgeOptions().addInboundPermitted(inboundPermitted).setPingTimeout(5000);

sockJSHandler.bridge(options, be -> {
  if (be.type() == BridgeEventType.SOCKET_IDLE) {
    // 執行某些處理
  }

  be.complete(true);
});

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

在客戶端 JavaScript 環境裏您使用 vertx-eventbus.js 來建立到 Event Bus 的鏈接併發送和接收消息:

<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() {

 // 設置一個接收消息的回調函數
 eb.registerHandler('some-address', function(error, message) {
   console.log('received a message: ' + JSON.stringify(message));
 });

 // 發送消息
 eb.send('some-address', {name: 'tim', age: 587});
}

</script>

在這個例子中,第一件事是建立了一個 Event Bus 實例:

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

構造函數的第二個參數是告訴 SockJS 的庫每隔 5 分鐘發送一個 ping 消息。因爲服務器端配置了指望每隔 5 秒收到一條 ping 消息,所以會在服務器端觸發 SOCKET_IDLE 事件。

您也能夠在處理事件時修改原始的消息內容,例如修改消息體。對於從客戶端發送來的消息,您也能夠修改消息的消息頭,下面是一個例子:

Router router = Router.router(vertx);

// 容許客戶端向地址 `demo.orderService` 發送消息
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) {
    // 添加消息頭
    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。它是一種能夠再未受權的網站獲取用戶隱私數據的技術。Vet.x-Web 提供了一個處理器 CSRFHandler 是您能夠避免跨站點的僞造請求。

這個處理器會向全部的 GET 請求的響應里加一個獨一無二的令牌做爲 Cookie。客戶端會在消息頭裏包含這個令牌。因爲令牌基於 Cookie,所以須要在 Router 上啓用 Cookie 處理器。

當開發非單頁面應用,並依賴客戶端來發送 POST 請求時,這個消息頭沒辦法在 HTML 表單裏指定。爲了解決這個問題,這個令牌的值也會經過表單屬性來檢查。這隻會發生在請求中不存在這個消息頭,而且表單中包含同名屬性時。例如:

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

您須要將表單的屬性設置爲正確的值。填充這個值惟一的辦法是經過上下文來獲取鍵 X-XSRF-TOKEN 的值。這個鍵的名稱也能夠在初始化 CSRFHandler 時指定。

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

});

虛機主機處理器

虛機主機處理器會驗證請求的主機名。若是匹配成功,則轉發這個請求到註冊的處理器上。不然,繼續在原先的處理器鏈中執行。

處理器經過請求的消息頭 Host 來進行匹配,並支持基於通配符的模式匹配。例如 *.vertx.io 或完整的域名 www.vertx.io

router.route().handler(VirtualHostHandler.create("*.vertx.io", routingContext -> {
  // 若是請求訪問虛機主機 `*.vertx.io` ,執行某些處理
}));

OAuth2 認證處理器

OAuth2AuthHandler 幫助您快速地配置基於 OAuth2 協議的安全路由。這個處理器簡化了獲取 authCode 的流程。下面的例子用這個處理器實現了保護資源並經過 GitHub 來受權:

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

// 在服務器上建立 oauth2 處理器
// 第二個參數是您提供給您的提供商的回調 URL
OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProvider, "https://myserver.com/callback");

// 配置回調處理器來接收 GitHub 的回調
oauth2.setupCallback(router.route());

// 保護 `/protected` 路徑下的資源
router.route("/protected/*").handler(oauth2);
// 在 `/protected` 路徑下掛載某些處理器
router.route("/protected/somepage").handler(rc -> {
  rc.response().end("Welcome to the protected resource!");
});

// 歡迎頁
router.get("/").handler(ctx -> {
  ctx.response().putHeader("content-type", "text/html").end("Hello<br><a href=\"/protected/somepage\">Protected by Github</a>");
});
OAuth2Auth authProvider = GithubAuth.create(vertx, "CLIENT_ID", "CLIENT_SECRET");

// 在服務器上建立 oauth2 處理器
// 第二個參數是您提供給您的提供商的回調 URL
OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProvider, "https://myserver.com/callback");

// 配置回調處理器來接收 GitHub 的回調
oauth2.setupCallback(router.route());

// 保護 `/protected` 路徑下的資源
router.route("/protected/*").handler(oauth2);
// 在 `/protected` 路徑下掛載某些處理器
router.route("/protected/somepage").handler(rc -> {
  rc.response().end("Welcome to the protected resource!");
});

// 歡迎頁
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 發起了重載操做,則會由於驗證錯誤而請求失敗。

經驗法則是:當有效的回調執行時,通知客戶端跳轉到受保護的資源上。

就 OAuth2 規範的生態來看,使用其餘的 OAuth2 提供商須要做出少量的修改。爲此,Vertx Auth 提供了若干開箱即用的實現:

若是您須要使用一個上述未列出的提供商,您也可使用基本的 API 來實現,例如:

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

// 在域名 `http://localhost:8080` 上建立 oauth2 處理器
OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(authProvider, "http://localhost:8080");

// 配置須要的權限
oauth2.addAuthority("profile");

// 配置回調處理器來接收 Google 的回調
oauth2.setupCallback(router.get("/callback"));

// 保護 `/protected` 路徑下的資源
router.route("/protected/*").handler(oauth2);
// 在 `/protected` 路徑下掛載某些處理器
router.route("/protected/somepage").handler(rc -> {
  rc.response().end("Welcome to the protected resource!");
});

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

您須要手工提供全部關於您所使用的提供商的細節,但結果是同樣的。

這個處理器會在您的應用上綁定回調的 URL。用法很簡單,只須要爲這個處理器提供一個路由(Route),其餘的配置都會自動完成。一個典型的狀況是您的 OAuth2 提供商會須要您來提供您的應用的 callback url,則您的輸入相似於 https://myserver.com/callback。這是您的處理器的第二個參數。至此,您完成全部必須的配置,只須要經過 setupCallback 方法來啓動它便可。

以上就是如何在您的服務器上綁定處理器 https://myserver.com:8447/callback注意,端口號能夠不使用默認值。

OAuth2AuthHandler oauth2 = OAuth2AuthHandler.create(provider, "https://myserver.com:8447/callback");
// 容許該處理器爲您處理回調地址
oauth2.setupCallback(router.route());

在這個例子中,Route 對象經過 Router.route() 建立。若是您須要完整的控制處理器的執行順序(例如您指望它在處理鏈中首先被執行),您也能夠先建立這個 Route 對象,而後將引用傳進這個方法裏。

混合 OAuth2 和 JWT

一些 OAuth2 的提供商參考了 RFC6750 規範,使用 JWT 令牌來做爲訪問令牌。這對於須要混合基於客戶端的受權和基於 API 的受權頗有用。例如您的應用提供了一些受保護的 HTML 文檔,同時您又但願他能夠做爲 API 被消費。在這種狀況下,一個 API 不可以很容易的處理 OAuth2 須要的重定向握手,但能夠提供令牌(11)。

只要提供商被配置爲支持 JWT,OAuth 處理器會自動處理這個問題。

這意味着您的 API 能夠經過提供值爲 Bearer BASE64_ACCESS_TOKEN 的消息頭 Authorization 來訪問受保護的資源。

註釋

  1. 指 HTTP 協議的 Method
  2. 內容協商 指容許同一個 URI 能夠根據請求中的 Accept 字段來提供資源的不一樣版本。
  3. accept 指 Router 的 accept 方法。示例代碼使用了 Java 8 Lambda 的 方法引用 語法。
  4. Reroute 一詞沒有找到合適的方式來描述,譯爲了 轉發。此處有別於 HTTP 的 Redirect 或 Proxy 等概念,只是進程內的邏輯跳轉。
  5. 會話 Cookie 也即 Session Cookie,特指有效期爲 session 的 Cookie。可參考 MSDN
  6. 或可稱之爲不可枚舉的。可防止碰撞攻擊。
  7. 指經過 vertx.executeBlocking 方法來按期刷新生成器的種子,在 Event Loop 線程中同步執行生成隨機數的過程。
  8. 即 Route.failureHandler
  9. 實際上不一樣的 transport 具備不一樣的會話處理機制。sessionTimeout 主要針對輪詢方式的 transport,例如 xhr。服務器端返回一個響應以後,客戶端一旦接受了響應,會馬上再發一個 request 出來繼續等下一個消息。若是超過了默認的 5 秒該會話沒有收到新的請求,則會認爲客戶端斷開了鏈接,會話過時。
  10. 關於 OAuth2 如何經過 JWT 來進行受權,能夠 參考這裏

結語

route 一詞同時具備名詞和動詞的含義。爲了不混淆,原文中全部使用名詞的地方都統一按照專有名詞 Route / route 處理。原文中的動詞統一譯爲 路由。原文的最後幾部分關於 SockJS 和 OAuth2 的內容寫做風格明顯和前文不一樣,並且有些地方描述的很簡略(例如 OAuth 流程的細節、SockJS 的不一樣 Transport 之間的差別等)。本着翻譯準確的原則,本譯文沒有進一步展開描述。

相關文章
相關標籤/搜索