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 應用的構建模塊。
可以把它想象成一把構建現代的、可伸縮的 Web 應用的瑞士軍刀。
Vert.x Core 提供了一系列底層的功能用於操作 HTTP,對於一部分應用來是足夠的。
Vert.x Web 基於 Vert.x Core,提供了一系列更豐富的功能以便更容易地開發實際的 Web 應用。
它繼承了 Vert.x 2.x 里的 Yoke 的特點,靈感來自於 Node.js 的框架 Express 和 Ruby 的框架 Sinatra 等等。
Vert.x Web 的設計是強大的,非侵入式的,並且是完全可插拔的。Vert.x 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),因此您隨時可以實現您自己的處理器。我們預計隨着時間的推移會有更多的處理器被實現。
我們會在本手冊里討論所有上述的特性。
使用 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 ,您也可以使用對應的 get
、post
、put
等方法。下面是一個例子:
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
對象來描述,通過這個對象可以獲得名稱、文件名、大小等屬性。
處理 Cookie
Vert.x Web 通過 Cookie 處理器 CookieHandler
來支持 cookie。
您需要保證 cookie 處理器器能夠匹配到所有您需要這個功能的請求。
router.route().handler(CookieHandler.create());
操作 Cookie
您可以使用 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.: SHA1PRNGio.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
方法來刪除數據。
會話中的鍵的類型必須是字符串。本地會話存儲的值可以是任何類型;集群會話存儲的值類型可以是基本類型,或者 Buffer
、JsonObject
、JsonArray
或可序列化對象。因為這些值需要在集群中進行序列化。
以下是操作會話數據的例子:
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-control
、last-modified
、date
。
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 的網站 中找到,簡單來說您會像這樣使用:
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
對象:
這個配置精確地定義了消息可以被發送到哪些地址。如果您需要通過精確的地址來控制消息的話,使用這個選項。
這個配置通過正則表達式來定義消息可以被發送到哪些地址。如果您需要通過正則表達式來控制消息的話,使用這個選項。如果指定了 address
,這個選項會被忽略。
這個配置通過消息的接口來控制消息是否可以發送。這個配置中定義的每一個字段必須在消息中存在,並且值一致。這個配置只能用於 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 提供了若干開箱即用的實現:
- Azure Active Directory
AzureADAuth
- Box.com
BoxAuth
- Dropbox
DropboxAuth
- Facebook
FacebookAuth
- Foursquare
FoursquareAuth
- Github
GithubAuth
- Google
GoogleAuth
- Instagram
InstagramAuth
- Keycloak
KeycloakAuth
- LinkedIn
LinkedInAuth
- Mailchimp
MailchimpAuth
- Salesforce
SalesforceAuth
- Shopify
ShopifyAuth
- Soundcloud
SoundcloudAuth
- Stripe
StripeAuth
- Twitter
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")); // 在域名 `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
來訪問受保護的資源。
注釋
- 指 HTTP 協議的 Method
- 內容協商 指允許同一個 URI 可以根據請求中的 Accept 字段來提供資源的不同版本。
- accept 指 Router 的
accept
方法。示例代碼使用了 Java 8 Lambda 的 方法引用 語法。 - Reroute 一詞沒有找到合適的方式來描述,譯為了
轉發
。此處有別於 HTTP 的 Redirect 或 Proxy 等概念,只是進程內的邏輯跳轉。 - 會話 Cookie 也即 Session Cookie,特指有效期為
session
的 Cookie。可參考 MSDN。 - 或可稱之為不可枚舉的。可防止碰撞攻擊。
- 指通過
vertx.executeBlocking
方法來定期刷新生成器的種子,在 Event Loop 線程中同步執行生成隨機數的過程。 - 即
Route.failureHandler
。 - 實際上不同的 transport 具有不同的會話處理機制。sessionTimeout 主要針對輪詢方式的 transport,例如 xhr。服務器端返回一個響應之后,客戶端一旦接受了響應,會立刻再發一個 request 出來繼續等下一個消息。如果超過了默認的 5 秒該會話沒有收到新的請求,則會認為客戶端斷開了連接,會話過期。
- 關於 OAuth2 如何通過 JWT 來進行授權,可以 參考這里。
結語
route
一詞同時具有名詞和動詞的含義。為了避免混淆,原文中所有使用名詞的地方都統一按照專有名詞 Route / route 處理。原文中的動詞統一譯為 路由
。原文的最后幾部分關於 SockJS
和 OAuth2
的內容寫作風格明顯和前文不同,而且有些地方描述的很簡略(例如 OAuth 流程的細節、SockJS 的不同 Transport 之間的差異等)。本着翻譯准確的原則,本譯文沒有進一步展開描述。