<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>5.1.10</version>
</dependency>
Vert.x-Web
Vert.x-Web 是一套用于使用 Vert.x 构建 Web 应用程序的基础积木组件。你可以将其视为构建现代化、高伸缩性 Web 应用的“瑞士军刀”。
Vert.x Core 提供了用于处理 HTTP 的低阶基础功能,对于某些简单场景,这些功能已经足够。
Vert.x-Web 则在 Vert.x Core 之上构建,提供了更为丰富、完备的特性集,帮助你更轻松、高效地开发真实的现代化 Web 应用程序。
Vert.x-Web 秉持强大、无强加约束(Un-opinionated)且完全可嵌入的设计理念。你只需要按需选择所需功能模块即可,Vert.x-Web 绝非笨重的应用容器。
你可以使用 Vert.x-Web 创建经典的传统服务端渲染 Web 应用、RESTful Web 微服务、实时推送(Server Push)应用,或任何你能想到的其他 Web 服务架构。Vert.x-Web 从不设限,应用的技术选型与形态完全由你决定。
Vert.x-Web 非常适合编写高性能 RESTful HTTP 微服务,但我们绝不强求你必须采用某种特定的开发范式。
Vert.x-Web 的核心特性包括:
-
路由分发(支持基于 HTTP Method、URL Path 等多维度匹配)
-
基于正则表达式的路径模式匹配
-
从 URL 路径中提取占位符与命名参数
-
内容协商(Content Negotiation)
-
请求体全功能处理(Request Body Handling)
-
请求体体积上限限制
-
Multipart 表单解析
-
Multipart 大文件流式上传
-
子路由器(Sub-routers)树状挂载与拆分
-
会话(Session)支持 —— 兼顾本地 Sticky Session 与分布式集群非粘性 Session
-
跨域资源共享(CORS)支持
-
统一错误处理与错误页 Handler
-
HTTP Basic / Digest 身份认证
-
基于重定向的登录认证流程
-
细粒度授权判定 Handler
-
基于 JWT / OAuth2 的安全鉴权
-
用户 / 角色 / 权限多维权限模型
-
Favicon 静态图标自动服务
-
服务端渲染模板引擎开箱即用支持:
-
Handlebars
-
Pug
-
MVEL
-
Thymeleaf
-
Apache FreeMarker
-
Pebble
-
Rocker
-
-
响应时间耗时统计 Handler
-
静态资源文件服务(内置客户端与服务端缓存、目录索引列表等能力)
-
请求超时熔断控制
-
SockJS 实时长连接支持
-
EventBus 事件总线前端桥接(SockJS Event-Bus Bridge)
-
跨站请求伪造(CSRF)防护
-
虚拟主机(VirtualHost)域名分发
Vert.x-Web 的绝大多数特性均通过模块化的 Handler 实现,你随时可以编写自定义 Handler 扩展功能。随着社区演进,更多优质 Handler 也在不断涌现。
本手册将全面详尽地介绍这些特性与实战用法。
使用 Vert.x Web
若要在项目中使用 Vert.x Web,请将以下依赖项添加到构建描述文件的 dependencies 配置节中:
-
Maven(在
pom.xml中):
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web:5.1.10'
}
开发模式
Vert.x Web 默认运行于生产模式(Production Mode)。
你可以通过将以下任意一项设置为 dev 来切换至开发模式:
-
环境变量
VERTXWEB_ENVIRONMENT,或 -
Java 系统属性
vertxweb.environment
在开发模式下:
-
模板引擎缓存将被自动禁用(实现模版修改实时生效)
-
ErrorHandler错误处理器会展示详细的异常堆栈信息 -
StaticHandler静态资源处理器会跳过客户端强缓存头处理 -
禁用 GraphiQL 生产管控工具
回顾 Vert.x Core HTTP 服务器
Vert.x-Web 深度使用并完全透传了 Vert.x Core 的底层 API,因此如果你此前尚未接触过 Vert.x Core HTTP 服务器的基础概念,强烈建议先阅读 Core 文档进行快速熟悉。
Vert.x Core HTTP 文档对此有非常详尽的技术讲解。
以下是使用 Vert.x Core 编写的一个最基础的 Hello World HTTP 服务器(此时尚未引入 Vert.x-Web):
HttpServer server = vertx.createHttpServer();
server.requestHandler(request -> {
// 每当有新 HTTP 请求到达服务器时,该 Handler 就会被触发执行
HttpServerResponse response = request.response();
response.putHeader("content-type", "text/plain");
// 写入响应体数据并结束该 HTTP 事务
response.end("Hello World!");
});
server.listen(8080);
我们创建了一个 HTTP 服务器实例,并为其设置了请求处理器(Request Handler)。每当有请求到达服务器,该 Handler 就会被回调执行。
在 Handler 内部,我们将响应头的 Content-Type 设置为 text/plain,写入 Hello World! 字符串,并调用 end() 结束响应。
最后,我们命令服务器监听 8080 端口(默认绑定主机为 localhost)。
你可以运行该代码并在浏览器中访问 http://localhost:8080,验证其运行效果。
Vert.x-Web 核心基础概念
从架构全局俯瞰:
路由器在收到传入的 HttpServerRequest 时,首先会暂停(Pause)请求流以确保请求体数据或 WebSocket 等协议升级信号不会在分派前丢失;接着,它会按照规则查找与该请求匹配的第一个路由,并将请求传递给该路由执行。
每个 Route 都可以绑定一个或多个 Handler(处理器)来接收请求。你可以在 Handler 中执行具体的业务逻辑,随后要么调用 end() 完成响应,要么调用 next() 将请求流转给下一个匹配的 Handler 继续处理。
以下是一个简单的 Router 示例:
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
router.route().handler(ctx -> {
// 该处理器将拦截处理进入服务器的每一个请求
HttpServerResponse response = ctx.response();
response.putHeader("content-type", "text/plain");
// 写入响应并结束响应流程
response.end("Hello World from Vert.x-Web!");
});
server.requestHandler(router).listen(8080);
这段代码实现的功能与上一节中纯 Vert.x Core 的示例一致,但这次由 Vert.x-Web 承载。
与之前一样,我们先创建一个 HTTP 服务器,随后创建一个 Router 路由器。我们在路由器上注册了一条不带任何过滤条件的通用路由,因此它会匹配到达服务器的 所有 请求。
随后我们为该路由指定了一个 Handler。
传递给 Handler 的参数对象是 RoutingContext(路由上下文)—— 它不仅包含了原生的 HttpServerRequest 与 HttpServerResponse,还封装了大量开箱即用的上下文工具,使 Web 开发极为高效。
对于每一个被路由的请求,系统都会创建唯一的路由上下文实例,并在该请求贯穿的所有 Handler 之间共享传递。
设置好路由处理器后,我们将 HTTP 服务器的请求处理器委托给路由器的 handle 方法。
以上就是核心基础。接下来我们将深入剖析各个具体特性的细节:
原始路径 vs 规范化路径
在处理路由时,必须注意:HttpServerRequest#path() 返回的是客户端发送的 原始 (Raw) 请求路径。该原始路径可能包含 .、.. 或连续重复的 / 等特殊字符段。
Vert.x Web 内部在执行路由匹配时,使用的是经过 规范化 (Normalized) 处理后的路径。如果你在 Handler 内部需要获取规范化路径,请调用 RoutingContext#normalizedPath():
router.route("/secured/*").handler(ctx -> {
// 客户端发送的原始路径(可能包含类似 ".." 的回溯符)
String raw = ctx.request().path();
// 路由器内部用于路由匹配的规范化路径
String normalized = ctx.normalizedPath();
// 对于涉及安全鉴权或权限校验的逻辑,强烈推荐使用规范化路径
if ("/secured/data".equals(normalized)) {
// ...
}
});
|
在进行依赖请求路径的审计日志记录或授权鉴权决策时,请优先使用 |
处理请求与调用下一个处理器
当 Vert.x-Web 将请求分发到匹配的路由时,会调用该路由的 Handler 并传入 RoutingContext 实例。一条路由可以按序追加挂载多个 Handler,通过 handler 即可完成添加。
如果在你的 Handler 中没有结束响应(即未调用 response.end()),那么你应该显式调用 next,以便将请求流转给下一个匹配的处理器或后续路由继续处理。
你并不一定要在 Handler 同步执行完毕时立即调用 next;你完全可以在异步操作(如定时器、异步 I/O、数据库查询)完成后再调用它:
Route route = router.route("/some/path/");
route.handler(ctx -> {
HttpServerResponse response = ctx.response();
// 启用分块传输编码(Chunked),因为后续 Handler 会陆续追加输出内容。
// 这一步只需在首个输出内容的 Handler 中设置一次。
response.setChunked(true);
response.write("route1\n");
// 延时 5 秒后唤醒下一个匹配路由继续处理
ctx.vertx().setTimer(5000, tid -> ctx.next());
});
route.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.write("route2\n");
// 再次延时 5 秒后唤醒下一个匹配路由
ctx.vertx().setTimer(5000, tid -> ctx.next());
});
route.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.write("route3");
// 此时结束响应
ctx.response().end();
});
在上面的示例中,首先向客户端写入 route1,5 秒后写入 route2,再过 5 秒写入 route3 并关闭连接。
请注意,整个流式异步处理过程完全基于非阻塞事件驱动,没有任何操作系统线程被挂起或阻塞。
简易响应
Handler 提供了极其强大的底层控制力,足以构建复杂的企业级系统。而对于直接从 Vert.x 异步 API 返回数据的简单响应场景,路由器还提供了一个快捷方法 respond:
-
自动将返回值序列化为 JSON 格式并输出。
-
若 Handler 执行抛出异常,自动返回对应的规范化 HTTP 错误。
-
若对象在 JSON 序列化时发生异常,同样会自动返回对应的错误响应。
router
.get("/some/path")
// 该处理器会自动将返回值序列化为 JSON,并自动设置 Content-Type 为 "application/json"
.respond(
ctx -> Future.succeededFuture(new JsonObject().put("hello", "world")));
router
.get("/some/path")
// 该处理器支持将任意普通 Java POJO 对象序列化为 JSON 响应
.respond(
ctx -> Future.succeededFuture(new Pojo()));
当然,如果你在传入的函数中显式调用了 write 或 end,它也可以用于非 JSON 的文本或自定义流式响应:
router
.get("/some/path")
.respond(
ctx -> ctx
.response()
.putHeader("Content-Type", "text/plain")
.end("hello world!"));
router
.get("/some/path")
// 在这种情况下,该处理器负责确保连接正常流式写入并终止
.respond(
ctx -> ctx
.response()
.setChunked(true)
.write("写入一些文本数据..."));
使用阻塞型处理器
有时你可能不得不在 Handler 中执行可能阻塞 Event Loop 的同步任务,例如调用遗留的第三方阻塞 SDK、同步 JDBC 或执行密集耗时的 CPU 计算。
在常规的 Event Loop Handler 中绝对严禁执行阻塞操作。为此,Vert.x-Web 允许在路由上注册专门的阻塞型处理器(Blocking Handler)。
阻塞型处理器的编写体验与普通 Handler 完全一致,但 Vert.x 会将其提交到 Worker 线程池中执行,绝不会占用 Event Loop 线程。
你可以通过 blockingHandler 在路由上注册阻塞处理器,如下所示:
router.route().blockingHandler(ctx -> {
// 同步执行耗时较长的阻塞方法
service.doSomethingThatBlocks();
// 执行完毕后调用 next 流转到下一个 Handler
ctx.next();
});
默认情况下,在同一个上下文环境(如同一个 Verticle 实例)中挂载的阻塞型处理器是 按序执行(Ordered) 的 —— 即前一个阻塞任务未执行完毕前,下一个阻塞任务不会开始。如果你不介意执行顺序且希望多个阻塞处理器并行并发执行,可以通过带布尔值的重载方法 blockingHandler 将 ordered 参数指定为 false。
注意:如果你需要在阻塞型处理器中处理 Multipart 表单数据,必须 先 使用一个非阻塞 Handler 调用 setExpectMultipart(true)。示例:
router.post("/some/endpoint").handler(ctx -> {
ctx.request().setExpectMultipart(true);
ctx.next();
}).blockingHandler(ctx -> {
// ... 在 Worker 线程中执行具体的阻塞业务
});
按精确路径路由
路由可以配置为匹配请求 URI 中的精确路径。此时它仅会匹配与指定路径完全相同的请求。
在以下示例中,处理器会拦截对 /some/path/ 的请求。Vert.x-Web 对末尾斜杠进行了容错处理,因此 /some/path 和 /some/path// 同样可以匹配:
Route route = router.route().path("/some/path/");
route.handler(ctx -> {
// 该处理器将被以下请求路径触发:
// `/some/path/`
// `/some/path//`
//
// 但不会匹配:
// `/some/path`(末尾显式指定斜杠时规则为严格匹配)
// `/some/path/subdir`
});
// 不以斜杠结尾的路径属于非严格模式,末尾的斜杠可选,两者均可匹配:
Route route2 = router.route().path("/some/path");
route2.handler(ctx -> {
// 该处理器将被以下请求路径触发:
// `/some/path`
// `/some/path/`
// `/some/path//`
//
// 但不会匹配:
// `/some/path/subdir`
});
按路径前缀通配路由
在实际开发中,通常希望统一路由以特定前缀开头的所有请求。虽然可以使用正则表达式达成,但更简单直观的做法是在声明路由路径时在末尾加上星号 *。
在以下示例中,处理器将匹配 URI 路径以 /some/path/ 开头的任意请求。
例如 /some/path/foo.html 和 /some/path/otherdir/blah.css 都会成功匹配。
Route route = router.route().path("/some/path/*");
route.handler(ctx -> {
// 该处理器会匹配所有以 `/some/path/` 开头的请求,例如:
// `/some/path/`
// `/some/path/subdir`
// `/some/path/subdir/blah.html`
//
// 但 **不会** 匹配:
// `/some/path`(通配符前的末尾斜杠不可省略,以保证子路由与组合模式不会出现意外误配)
// `/some/patha`
// `/some/patha/`
// 等等...
});
创建 Route 对象时也可以直接将路径传入:
Route route = router.route("/some/path/*");
route.handler(ctx -> {
// 匹配效果与上例完全相同
});
捕获路径参数
路由器支持在路径中使用占位符匹配动态参数,提取出的参数值可以通过 pathParam 在路由上下文中随时获取。
示例:
router
.route(HttpMethod.POST, "/catalogue/products/:productType/:productID/")
.handler(ctx -> {
String productType = ctx.pathParam("productType");
String productID = ctx.pathParam("productID");
// 基于捕获的参数执行业务操作...
});
占位符由冒号 : 紧跟参数名构成。默认情况下参数名可包含字母、数字及下划线。如果你需要支持更多字符,可以启用扩展模式以支持 - 与 $。通过以下系统属性开启扩展参数规则:
-Dio.vertx.web.route.param.extended-pattern=true
在上述示例中,若客户端向 /catalogue/products/tools/drill123/ 发起 POST 请求,路由即会命中,此时 productType 的值为 tools,productID 的值为 drill123。
参数并不局限于必须占据一整个 URL 路径段。例如以下形式的路径参数也是完全合法的:
router
.route(HttpMethod.GET, "/flights/:from-:to")
.handler(ctx -> {
// 处理发往 /flights/AMS-SFO 的请求时将分别捕获:
String from = ctx.pathParam("from"); // AMS
String to = ctx.pathParam("to"); // SFO
// 请注意:如果未开启前文提到的 extended 扩展参数模式,
// "-" 会被视作变量名的一部分而非分隔符。
});
注意:通配符 同样可以使用 pathParam("") 进行提取。
使用正则表达式进行路由
你也可以使用正则表达式来精确匹配请求的 URI 路径。
Route route = router.route().pathRegex(".*foo");
route.handler(ctx -> {
// 该处理器将被以下请求命中:
// /some/path/foo
// /foo
// /foo/bar/wibble/foo
// /bar/foo
// 但不会匹配:
// /bar/wibble
});
同样,可以在创建 Route 时直接传入正则表达式:
Route route = router.routeWithRegex(".*foo");
route.handler(ctx -> {
// 效果与上例相同
});
使用正则表达式捕获路径参数
在使用正则表达式时,同样可以利用捕获组提取路径参数:
Route route = router.routeWithRegex(".*foo");
// 该正则表达式匹配形如 "/foo/bar" 的路径,其中 "foo" 会被捕获为 param0,
// 而 "bar" 则会被捕获为 param1
route.pathRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(ctx -> {
String productType = ctx.pathParam("param0");
String productID = ctx.pathParam("param1");
// 执行业务逻辑...
});
在上例中,若请求路径为 /tools/drill123/,路由即会命中,此时 productType 的值为 tools,productID 为 drill123。
捕获组在正则表达式中通过圆括号 (…) 显式标识。
使用具名捕获组
在某些场景下,使用数字索引的参数名(如 param0)容易混淆且不易维护。你可以在正则表达式中使用具名捕获组(Named Capture Groups):
router
.routeWithRegex("\\/(?<productType>[^\\/]+)\\/([^\\/]+)")
.handler(ctx -> {
String productType = ctx.pathParam("productType");
String productID = ctx.pathParam("param1");
// 执行业务逻辑...
});
在上述示例中,具名捕获组会直接映射到与组名相同的路径参数上。同时,你依然可以通过传统索引方式(param0、param1…)访问这些捕获组。
按 HTTP 请求方法路由
默认情况下,一条路由会匹配所有的 HTTP 方法(GET、POST、PUT 等)。
如果你希望路由仅响应特定的 HTTP 方法,可以使用 method:
Route route = router.route().method(HttpMethod.POST);
route.handler(ctx -> {
// 该处理器仅响应 POST 请求
});
也可以在创建 Route 时同时指定路径和方法:
Route route = router.route(HttpMethod.POST, "/some/path/");
route.handler(ctx -> {
// 仅响应发往以 /some/path/ 开头的 POST 请求
});
router.get().handler(ctx -> {
// 匹配所有 GET 请求
});
router.get("/some/path/").handler(ctx -> {
// 匹配所有发往以 /some/path/ 开头的 GET 请求
});
router.getWithRegex(".*foo").handler(ctx -> {
// 匹配所有路径以 foo 结尾的 GET 请求
});
如果你希望一条路由同时响应多种 HTTP 方法,可以多次调用 method:
Route route = router.route().method(HttpMethod.POST).method(HttpMethod.PUT);
route.handler(ctx -> {
// 同时响应 POST 或 PUT 请求
});
如果你构建的应用需要支持非标准的自定义 HTTP 动词(例如 WebDav 服务器中的扩展方法),你可以使用 HttpMethod.valueOf:
Route route = router.route()
.method(HttpMethod.valueOf("MKCOL"))
.handler(ctx -> {
// 拦截处理 MKCOL 请求
});
请注意,重路由(Reroute)等特性不接受自定义 HTTP 动词;此外在检查路由所含动词时,自定义动词会返回枚举值 OTHER 而非自定义名称。
|
路由执行顺序
默认情况下,路由按照它们被添加到 Router 的先后顺序依次进行匹配求值。
当有新请求到达时,Router 会自上而下逐一检查各个路由是否匹配。一旦匹配成功,就会执行该路由绑定的 Handler。
若该 Handler 在处理完毕后调用了 next,则 Router 会继续寻找下一个匹配的路由(若存在)并调用其 Handler,依此类推。
以下示例直观展示了这一执行链:
router
.route("/some/path/")
.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.setChunked(true);
response.write("route1\n");
// 调用下一个匹配的路由
ctx.next();
});
router
.route("/some/path/")
.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.write("route2\n");
// 调用下一个匹配的路由
ctx.next();
});
router
.route("/some/path/")
.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.write("route3");
// 终止响应
ctx.response().end();
});
对于发往 /some/path 的请求,最终客户端收到的响应体内容为:
route1 route2 route3
如果你希望覆盖默认的先后添加顺序,可以使用 order 显式指定整型优先级数值。
系统在创建路由时会自动为其赋予一个递增的 order 值(首条路由为 0,第二条为 1,依此类推)。
通过显式指定 order,你可以重设其匹配优先级。order 支持负数值(例如希望某条拦截路由在默认的第 0 号路由之前优先执行)。
我们来调整 route2 的顺序,使其优先于 route1 执行:
router
.route("/some/path/")
.order(1)
.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.write("route1\n");
ctx.next();
});
router
.route("/some/path/")
.order(0)
.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.setChunked(true);
response.write("route2\n");
ctx.next();
});
router
.route("/some/path/")
.order(2)
.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.write("route3");
ctx.response().end();
});
此时客户端收到的响应变为:
route2 route1 route3
若两个匹配路由的 order 值完全相同,则按照它们注册到 Router 的物理先后顺序执行。
此外,你还可以使用 last 将某条路由标记为在所有同级路由中最后执行。
注意:路由的 order 优先级必须在为其配置具体的 handler 之前完成设置!
基于请求 MIME 类型路由 (consumes)
你可以使用 consumes 声明该路由仅匹配特定 MIME 类型的请求。
此时,客户端发来的请求必须携带包含对应 MIME 类型的 Content-Type 请求头,并与 consumes 中声明的值完成匹配。
简而言之,consumes 描述的是当前处理器能够 消费/解析 的数据类型。
支持精确匹配:
router.route()
.consumes("text/html")
.handler(ctx -> {
// 仅响应 Content-Type 为 `text/html` 的请求
});
也支持声明多个精确类型:
router.route()
.consumes("text/html")
.consumes("text/plain")
.handler(ctx -> {
// 响应 Content-Type 为 `text/html` 或 `text/plain` 的请求
});
支持对子类型使用通配符匹配:
router.route()
.consumes("text/*")
.handler(ctx -> {
// 响应顶级类型为 text 的所有请求(如 text/html 或 text/plain 均可匹配)
});
同样支持对主类型通配:
router.route()
.consumes("*/json")
.handler(ctx -> {
// 响应子类型为 json 的所有请求(如 text/json 或 application/json 均可匹配)
});
若在 consumes 参数中没有显式书写 /,系统将默认将其作为子类型处理。
基于客户端可接受的 MIME 类型路由 (produces)
HTTP Accept 请求头用于告知服务端客户端期望接收何种 MIME 类型的响应数据。
一个 Accept 头中可以通过逗号 , 分隔指定多个 MIME 类型。
MIME 类型还可以携带 q 权重值(取值介于 0 到 1.0 之间,省略时默认为 1.0),用于在多个候选响应类型中声明偏好权重。
例如,以下 Accept 头表明客户端仅接受 text/plain:
Accept: text/plain
以下声明表示客户端同时接受 text/plain 或 text/html,且两者无优先级偏好:
Accept: text/plain, text/html
以下声明表示客户端同时接受两者,但优先偏好 text/html(因其权重 q=1.0 高于 0.9):
Accept: text/plain; q=0.9, text/html
若服务端能同时输出这两种格式,在此场景下应返回 text/html。
通过使用 produces,你可以声明当前路由能够生成(产出)何种 MIME 类型,例如以下路由生成 application/json 响应:
router.route()
.produces("application/json")
.handler(ctx -> {
HttpServerResponse response = ctx.response();
response.putHeader("content-type", "application/json");
response.end(someJSON);
});
此时,该路由将匹配 Accept 头兼容 application/json 的所有请求。
以下是可以命中的 Accept 头示例:
Accept: application/json Accept: application/* Accept: application/json, text/html Accept: application/json;q=0.7, text/html;q=0.8, text/plain
你也可以为一条路由声明支持产出多种不同的 MIME 类型。此时,可使用 getAcceptableContentType 获取客户端实际协商胜出的最佳 MIME 类型:
router.route()
.produces("application/json")
.produces("text/html")
.handler(ctx -> {
HttpServerResponse response = ctx.response();
// 获取经过内容协商后双方达成一致的最佳 MIME 类型
String acceptableContentType = ctx.getAcceptableContentType();
response.putHeader("content-type", acceptableContentType);
response.end(whatever);
});
在上述示例中,若客户端发送的请求头为:
Accept: application/json; q=0.7, text/html
路由成功匹配,此时 acceptableContentType 的值为 text/html(因为其权重更高)。
基于虚拟主机路由 (VirtualHost)
你可以配置一条 Route 仅匹配特定的请求主机名(Hostname)。
系统会校验客户端请求中的 Host 头,匹配模式支持使用 通配符,如 .vertx.io,或者配置完整域名如 www.vertx.io。
router.route().virtualHost("*.vertx.io").handler(ctx -> {
// 仅当请求域名满足 *.vertx.io 时执行处理
});
组合路由匹配条件
你可以将上述各种路由匹配条件自由组合使用,例如:
router.route(HttpMethod.PUT, "myapi/orders")
.consumes("application/json")
.produces("application/json")
.handler(ctx -> {
// 仅当请求为 PUT 方法、路径以 "myapi/orders" 开头、
// Content-Type 为 "application/json" 且 Accept 头兼容 "application/json" 时触发
});
代理转发请求头支持 (Forward Support)
在生产环境中,你的应用往往运行在反向代理(如 HAProxy、Nginx)之后。在这种部署架构下直接读取客户端网络连接信息通常无法获得真实数据(例如客户端 IP 会变成代理服务器的局域网 IP 而非终端用户 IP)。
为了传递真实的连接信息,业界标准化了专用的 Forwarded 请求头。但由于该标准推出较晚,许多代理服务器一直采用以 X-Forwarded- 为前缀的历史惯用请求头。Vert.x Web 支持解析这两种转发头,但出于安全考虑默认处于禁用状态。
之所以默认禁用,是为了防止外部恶意攻击者伪造自身请求头来伪装真实来源与客户端 IP。
要开启转发头解析支持,请显式配置:
router.allowForward(AllowForwardHeaders.FORWARD);
// 允许解析转发头,且在此模式下仅解析 "X-Forward" 系列请求头
router.allowForward(AllowForwardHeaders.X_FORWARD);
// 同时解析 RFC 标准的 "Forward" 头与非标准的 "X-Forward" 头;
// 若两者同时存在并产生冲突,以标准的 "Forward" 值为准,忽略 "X-Forward"
router.allowForward(AllowForwardHeaders.ALL);
如需显式关闭解析,可指定:
router.allowForward(AllowForwardHeaders.NONE);
若需深入了解请求头规范细节,请参考:
开启该特性后,Vert.x 底层会自动根据代理头修正连接(HTTP 或 WebSocket)的以下关键属性:
-
协议(Protocol/Scheme)
-
客户端主机名(Host Name)
-
客户端端口(Host Port)
上下文数据共享 (Context data)
你可以利用 RoutingContext 提供的上下文数据容器,在贯穿单次 HTTP 请求生命周期的各个 Handler 之间安全共享与传递任意业务对象。
以下示例展示了一个 Handler 存入数据、后续 Handler 读取数据的用法:
发往 /some/path/other 的请求会依次匹配这两条路由:
router.get("/some/path").handler(ctx -> {
ctx.put("foo", "bar");
ctx.next();
});
router.get("/some/path/other").handler(ctx -> {
String bar = ctx.get("foo");
// 基于传递的 bar 变量继续处理
ctx.response().end();
});
或者,你也可以通过 data 直接获取整个底层的上下文数据 Map。
路由元数据 (Metadata)
虽然上下文数据允许你在请求-响应生命周期内传递动态数据,但在很多架构场景中,为路由附加只读的运行时元数据(Metadata)同样至关重要 —— 例如用于自动生成 OpenAPI/Swagger 接口文档,或在启动时为特定路由挂载静态配置。
元数据特性的用法与上下文数据高度相似。你可以通过直接访问底层的 Map,或使用 Router 与 Route 接口上定义的专用 Getter 和 Setter:
router
.route("/metadata/route")
.putMetadata("metadata-key", "123")
.handler(ctx -> {
Route route = ctx.currentRoute();
String value = route.getMetadata("metadata-key"); // 123
// 以 123 作为响应体结束当前请求
ctx.end(value);
});
上下文辅助快捷方法 (Helper functions)
尽管 RoutingContext 允许你访问原生的底层 Request 和 Response 对象,但在实际编码中,直接使用系统提供的一批高频快捷辅助方法可以大幅提升生产效率:
下载附件(Attachment):该响应会自动设置对应的标头,引导浏览器唤起系统本地配置的对应 MIME 应用程序来打开该文件。比如在生成 PDF 报表时:
ctx
.attachment("weekly-report.pdf")
.end(pdfBuffer);
重定向(Redirect)到其他页面或域名(如跳转到 HTTPS 安全站点):
ctx.redirect("https://securesite.com/");
// 特殊快捷目标 "back":自动将用户重定向回 Referrer 来源页面;若无来源则默认返回 "/"
ctx.redirect("back");
向客户端快捷发送 JSON 数据响应:
ctx.json(new JsonObject().put("hello", "vert.x"));
// 同样支持 JsonArray 数组
ctx.json(new JsonArray().add("vertx").add("web"));
// 也可以传入任意 Java 对象,系统会根据运行时的 JSON 编码器自动完成序列化输出
ctx.json(someObject);
简便的内容类型匹配检查:
ctx.is("html"); // => true
ctx.is("text/html"); // => true
// 当 Content-Type 为 application/json 时
ctx.is("application/json"); // => true
ctx.is("html"); // => false
校验客户端请求资源是否处于未过期的“新鲜”缓存状态(结合缓存标头、Last-Modified 与 ETag):
ctx.lastModified("Wed, 13 Jul 2011 18:30:00 GMT");
// 利用该值校验客户端请求的新鲜度
if (ctx.isFresh()) {
// 客户端本地缓存依然有效,我们可以直接短路返回 304 Not Modified
}
以及其他一系列自解释的常用快捷方法:
ctx.etag("W/123456789");
// 设置最后修改时间
ctx.lastModified("Wed, 13 Jul 2011 18:30:00 GMT");
// 快速结束响应
ctx.end();
ctx.end("body");
ctx.end(buffer);
内部重路由 (Reroute)
到目前为止介绍的所有机制均是按单一时间线顺序处理请求。但在某些业务场景下,你可能需要回退并重新发起路由分发。由于 Context 本身并不暴露前驱或后继 Handler 的静态拓扑信息(这些在运行时是动态计算的),Vert.x-Web 提供了从当前 Router 起点重新执行完整路由匹配的重路由机制:
router.get("/some/path").handler(ctx -> {
ctx.put("foo", "bar");
ctx.next();
});
router
.get("/some/path/B")
.handler(ctx -> ctx.response().end());
router
.get("/some/path")
.handler(ctx -> ctx.reroute("/some/path/B"));
在上述代码中,当请求到达 /some/path 时,首个 Handler 会向上下文写入数据,随后流转到下一个 Handler 并调用 reroute("/some/path/B"),从而使得请求被重新分发到 /some/path/B 对应的路由并终止。
你可以仅根据新路径进行重路由,也可以同时指定新的 HTTP 方法与路径。需要注意的是,基于 HTTP 方法的重路由需要格外谨慎,以防引入安全隐患(例如将通常安全的 GET 请求重路由为具有破坏性的 DELETE)。
重路由同样允许在 FailureHandler(失败处理器)中调用。但需要注意:一旦调用重路由,当前的 HTTP 状态码与 failure 异常信息会被重置。因此,目标处理器如果需要特定的错误状态码,应该自行重新设置,示例:
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();
}
});
需要特别明确的是,重路由作用于 path。如果跨重路由需要保留或补充状态数据,应该使用 RoutingContext 上下文对象。例如希望在重路由时追加额外参数:
router.get("/final-target").handler(ctx -> {
// 从此处继续执行业务...
});
// (将请求连带查询字符串一起重路由到 /final-target)
router.get().handler(ctx -> ctx.reroute("/final-target?variable=value"));
// 更优雅安全的方式是将参数存入 RoutingContext
router.get().handler(ctx -> ctx
.put("variable", "value")
.reroute("/final-target"));
重路由操作会重新解析 URL 查询参数(之前已解析的旧参数会被丢弃),同时静默剥离并忽略路径中的 HTML Hash 锚点片段,以确保重路由语义与常规原生请求保持严格一致。
如需将更多业务数据传递给新路由,请充分利用在整个 HTTP 事务生命周期内始终保活的 Context。
子路由器 (Sub-routers)
当应用规模膨胀且包含大量 Handler 时,将它们按业务拆分到多个独立的 Router 中会更加清晰。当你希望将一套现成的 Handler 集合复用到不同系统、并挂载到不同的路径前缀下时,子路由器机制同样非常适用。
你可以将一个路由器作为 子路由器 (Sub-router) 挂载到父路由器的某个 挂载点 (Mount Point) 上。子路由器本身还可以继续嵌套挂载其他更深层次的子路由器。
以下展示了一个将子路由器挂载到主路由器的简易示例。
子路由器维护了一套模拟的 REST API 接口集合,我们将把它挂载到主路由器上(此处省略 REST API 的底层具体实现):
Router restAPI = Router.router(vertx);
restAPI.get("/products/:productID").handler(ctx -> {
// TODO 查询指定商品信息并返回....
ctx.response().write(productJSON);
});
restAPI.put("/products/:productID").handler(ctx -> {
// TODO 新增或修改商品...
ctx.response().end();
});
restAPI.delete("/products/:productID").handler(ctx -> {
// TODO 删除商品...
ctx.response().end();
});
若将该路由器作为顶层独立路由器使用,则直接请求 /products/product1234 即可命中该 API。
现在假设我们已经有了一个定义在另一个路由器上的完整主站点:
Router mainRouter = Router.router(vertx);
// 处理静态资源文件
mainRouter.route("/static/*").handler(myStaticHandler);
mainRouter.route(".*\\.templ").handler(myTemplateHandler);
我们只需在主路由器上指定挂载点(例如 /productsAPI)并将子路由器挂载上去:
mainRouter.route("/productsAPI/*")
.subRouter(restAPI);
这意味着该 REST API 现在可以通过 /productsAPI/products/product1234 等完整路径进行对外访问。
使用子路由器时需要遵循以下规则约束:
-
路由路径的末尾必须以通配符
*结尾 -
路径中允许使用
:param占位符参数,但不支持复杂的正则表达式 -
在此次挂载调用前后,当前路由对象上只能注册 1 个处理 Handler(但可以在同路径的新 Route 对象上注册其他 Handler)
-
每个路径对象仅支持挂载 1 个子路由器
相关规则校验是在路由器被添加到 HTTP 服务器的那一刻动态执行的。由于子路由器的动态组合特性,构建期无法预先捕获配置错误,它们依赖于上下文在运行时完成校验。
国际化与本地化 (Localization)
Vert.x Web 能够智能解析 HTTP Accept-Language 请求头,并提供了一系列工具方法,帮助你确定客户端首选的语言地区(Locale),或获取按优先级质量权重排序的语言列表。
Route route = router.get("/localized").handler(ctx -> {
// 通过遍历与条件分支,在以用户母语回复时,
// 能够严格遵循用户在 Accept-Language 中声明的偏好顺序。
for (LanguageHeader language : ctx.acceptableLanguages()) {
switch (language.tag()) {
case "en":
ctx.response().end("Hello!");
return;
case "zh":
case "zh-CN":
ctx.response().end("你好!");
return;
case "fr":
ctx.response().end("Bonjour!");
return;
case "pt":
ctx.response().end("Olá!");
return;
case "es":
ctx.response().end("Hola!");
return;
}
}
// 未匹配到受支持的语言,给出兜底提示:
ctx.response().end("抱歉,我们暂时不支持您的语言: " + ctx.preferredLanguage());
});
核心方法 acceptableLanguages 会返回用户客户端所支持的有序 Locale 列表;如果你只关心用户的首选语言,辅助方法 preferredLanguage 则会直接返回该列表的第一个元素(若用户未声明则返回 null)。
路由匹配失败状态码
当传入的请求未能匹配到任何有效路由时,Vert.x-Web 会根据未匹配的具体原因自动响应对应的标准 HTTP 错误状态码:
-
404:没有任何路由匹配请求的路径(Path)
-
405:有路由匹配路径,但不支持请求的 HTTP 方法(Method)
-
406:有路由匹配路径和方法,但无法产出满足客户端
Accept头要求的响应数据类型 -
415:有路由匹配路径和方法,但无法消费/接受请求的
Content-Type -
400:有路由匹配路径和方法,但无法接受空的请求体
你可以使用 errorHandler 手动接管并深度定制这些失败状态码的响应行为。
错误与异常处理 (Error handling)
除了设置常规的请求处理器外,你还可以为路由配置专门用于捕获并处理异常失败的 Failure Handler(失败处理器)。
失败处理器的路由匹配规则与普通处理器完全相同。
例如,你可以配置一个仅针对特定路径前缀或特定 HTTP 方法生效的失败处理器。这使得你能够为系统中的不同子模块量身定制差异化的容错处理策略。
以下示例展示了一个仅捕获发往 /somepath/ 的 GET 请求在路由过程中出现的失败的处理器:
Route route = router.get("/somepath/*");
route.failureHandler(ctx -> {
// 仅当路由发往以 '/somepath/' 开头的 GET 请求发生失败时被触发调用
});
在以下两种情况下会触发失败路由:一是某个 Handler 在执行期间抛出了未捕获的异常,二是某个 Handler 显式调用了 fail 并传入了特定的 HTTP 状态码以主动声明失败。
若 Handler 中抛出普通异常,系统会将其自动捕获并发出状态码为 500 的失败信号。
在处理失败时,失败处理器同样会接收到路由上下文参数,你可以从中提取 failure 异常实例或失败状态码,并据此构建友好的错误响应:
Route route1 = router.get("/somepath/path1/");
route1.handler(ctx -> {
// 模拟抛出运行时异常
throw new RuntimeException("发生未知异常!");
});
Route route2 = router.get("/somepath/path2");
route2.handler(ctx -> {
// 显式使请求失败,并指定状态码 403 Forbidden
ctx.fail(403);
});
// 定义统一失败处理器
// 上述任意处理器发生失败时均会被此处的 failureHandler 拦截
Route route3 = router.get("/somepath/*");
route3.failureHandler(failureRoutingContext -> {
int statusCode = failureRoutingContext.statusCode();
// 若由 RuntimeException 触发则 statusCode 为 500;
// 若由 ctx.fail(403) 触发则为 403
HttpServerResponse response = failureRoutingContext.response();
response.setStatusCode(statusCode).end("抱歉,当前请求无法被处理");
});
若在执行错误处理器时因状态消息头中使用了非法字符而导致内部二次报错,系统会自动将原始状态消息重置为对应错误码的默认消息。这是为了维持 HTTP 协议规范的稳定性与自愈性,避免底层 Socket 突兀中断而造成协议报文不完整。
请求体处理 (Request body handling)
BodyHandler 允许你获取 HTTP 请求体数据、限制请求体大小以及处理流式大文件上传。
对于任何需要读取请求体的接口,请务必确保在匹配该请求的路由链最前端预先挂载 BodyHandler。
由于 BodyHandler 需要注册处理器来消费 HTTP 请求体数据,它会自动恢复(Resume)此前被 Router 暂停的 HttpServerRequest。因此该处理器必须尽快被挂载。
router.route().handler(BodyHandler.create());
文件上传是常见的 DDoS 拒绝服务攻击来源。为减小攻击面,强烈建议通过 setBodyLimit 设置合理的体积上限(例如通用文件上传设置为 10MB,普通 JSON 接口设置为 100KB)。
|
获取请求体
如果你明确知道请求体是 JSON 格式,可以调用 body 并使用对应的 Getter(如 .asJsonObject() 或 .asJsonArray())。
若为普通文本,可调用 .asString();若需要原始二进制字节流,则调用 .buffer()。
限制请求体大小
要限制请求体大小,可在创建 BodyHandler 后调用 setBodyLimit 指定最大允许的字节数(单位:字节)。
这对于防范因超大请求体耗尽服务器内存至关重要。
若客户端尝试发送超出上限的请求体,Vert.x-Web 会自动响应 HTTP 状态码 413 Request Entity Too Large。
默认情况下,请求体大小上限限制为 10MB。
合并表单属性
默认情况下,BodyHandler 会将解析出的所有表单属性(Form Attributes)合并到请求的常规参数集(Parameters)中。如果你不需要这一行为,可以通过 setMergeFormAttributes 显式禁用。
处理文件上传
BodyHandler 同样用于处理 Multipart 多部件文件上传。
只要命中路由上挂载了 BodyHandler,任何上传的文件都会自动以流式方式落盘到指定的临时上传目录(默认为 file-uploads)。
系统会为每个上传的文件自动生成唯一文件名,上传的文件信息可以通过路由上下文的 fileUploads 统一获取。
示例:
router.route().handler(BodyHandler.create());
router.post("/some/path/uploads").handler(ctx -> {
List<FileUpload> uploads = ctx.fileUploads();
// 对上传的文件执行业务处理....
});
每个上传的文件均由 FileUpload 实例描述,允许你获取表单字段名、上传文件名、落盘路径及字节大小等属性。
上传临时目录清理策略
你可以配置 BodyHandler 在响应结束时自动清理临时文件:
router.route().handler(BodyHandler.create().setDeleteUploadedFilesOnEnd(true));
开启后,每当对应的 HTTP 响应发送完毕,当前请求所产生的所有临时上传文件都会被自动删除。
Cookie 机制与操作 (Handling cookies)
Vert.x-Web 开箱即用地提供了原生的 Cookie 支持。
操纵 Cookie
如需删除 Cookie,调用 removeCookie。
如需新增或回写 Cookie,调用 addCookie。
在响应头回写时,添加的 Cookie 会自动注入 Set-Cookie 响应头中由浏览器接收持久化。
Cookie 对象通过 Cookie 实例封装,你可以便捷地读取或设置名称、取值、域名(Domain)、生效路径(Path)、HttpOnly 及 Secure 等属性。
Cookie 查询与回写示例:
Cookie someCookie = ctx.request().getCookie("mycookie");
String cookieValue = someCookie.getValue();
// 处理业务逻辑...
// 添加新的 Cookie —— 该 Cookie 会自动回写到响应头中
ctx.response().addCookie(Cookie.cookie("othercookie", "somevalue"));
Session 会话管理 (Handling sessions)
Vert.x-Web 提供了完备的开箱即用会话管理功能。
会话贯穿多次连续的 HTTP 请求,只要浏览器会话处于活跃状态便始终保活,为跨请求暂存状态信息(如购物车)提供了理想场所。
Vert.x-Web 使用 Session Cookie 标识会话。该 Cookie 为临时 Cookie,在用户关闭浏览器后会被自动清除。
我们绝不会将实际的会话数据明文保存在客户端 Cookie 中 —— Cookie 仅存储一个随机唯一的会话标识符(Session ID),用于在服务端反查真实的 Session 数据。该 ID 采用高强度的安全伪随机数生成器(Secure Random)生成,具备极高的防猜测与抗碰撞能力。
鉴于 Cookie 会在 HTTP 请求与响应中跨网络明文传输,使用 Session 时强烈建议始终启用 HTTPS。如果在纯明文 HTTP 下使用 Session,Vert.x 会在控制台打印安全告警。
要在应用中启用会话机制,必须在业务 Handler 之前的路由链路上挂载 SessionHandler。
SessionHandler 会全自动接管 Session Cookie 的下发与服务端的反查加载,无需手动编写检索样板代码。
会话数据会在响应头发送给客户端后,自动异步写回并持久化到底层 SessionStore 中。但请注意:在此默认机制下,并不绝对保证在客户端收到响应报文的前一微秒数据已彻底落盘完成。 在某些需要强一致性保障的关键金融或状态流转场景下,你可以显式强制刷盘(Flush)。 强制刷盘会挂起自动保存流程(除非刷盘失败),允许你在完全确保会话持久化成功后再结束响应:
router.route().handler(ctx -> sessionHandler.flush(ctx)
.onSuccess(v -> ctx.end("保存成功!"))
.onFailure(err -> {
// 会话落盘失败...
// 降级执行应对预案
}));
Vert.x SessionHandler 默认依赖 Cookie 存储 Session ID。但如果客户端浏览器不支持 Cookie,或用户在浏览器设置中禁用了 Cookie,我们将无法把 Session ID 保存在客户端。此时每次新请求都会被动创建一个全新的空白会话,导致无法跨请求识别用户。
为此,Vert.x Web 提供了无需 Cookie 的“无 Cookie 会话 (Cookieless Session)”模式。在该模式下,Vert.x Web 会将 Session ID 编码嵌入到页面的 URL 路径中。页面内的所有跳转链接均会自动携带该 Session ID 标识。当用户点击链接时,系统从 URL 中提取 Session ID,从而在完全不依赖 Cookie 的前提下维持会话正常运作。
启用无 Cookie 会话:
router.route()
.handler(SessionHandler.create(store).setCookieless(true));
在无 Cookie 模式下,应用程序需要将 Session ID 动态渲染到 HTML 页面或前端脚本中。Session ID 在 URL 中的典型格式为 /optional/path/prefix/'('sessionId')'/path/suffix。
例如:http://localhost:2677/WebSite1/(S(3abhbgwjg33aqrt3uat2kh4d))/api/,其中的 Session ID 即为 3abhbgwjg33aqrt3uat2kh4d。
会话面临的主要安全风险是恶意攻击者窃取或猜解其他用户的 Session ID。一旦两名用户拥有相同的 Session ID,系统就会将其视为同一访客,导致敏感数据泄露或越权访问。当使用标准 Cookie 时,Session ID 可以通过 SSL 加密传输并通过标记 Secure 和 HttpOnly 进行全面防护;而在 Cookieless 模式下,Session ID 直接暴露在 URL 中,更容易被浏览器历史记录、Referrer 或代理日志泄漏,因此其安全风险明显更高。
会话存储后端 (Session stores)
创建 SessionHandler 必须传入对应的 SessionStore(会话存储)实例。SessionStore 是在服务端实际存储与管理会话状态的核心组件。
SessionStore 内部维护了一个安全的伪随机数生成器(PRNG),以确保生成的 Session ID 具备密码学安全等级。该 PRNG 独立于存储实例,这意味着给定 Store A 生成的 ID,外部绝不可能推导出 Store B 的 ID。
默认情况下,该 PRNG 采用混合模式:加盐播种阶段采用阻塞模式,生成阶段采用高并发非阻塞模式。PRNG 每隔 5 分钟会自动使用 64 位全新熵源进行二次播种。你可以通过以下 Java 系统属性进行深度调优:
-
io.vertx.ext.auth.prng.algorithm:例如SHA1PRNG -
io.vertx.ext.auth.prng.seed.interval:例如1000(每秒重新加盐) -
io.vertx.ext.auth.prng.seed.bits:例如128
绝大多数场景下均无需调整这些参数,除非发现 PRNG 算法在高并发下成为应用的性能瓶颈。
Vert.x-Web 开箱即用提供了两种核心存储实现,同时也支持编写自定义的 SessionStore。
各实现遵循 Java ServiceLoader SPI 规范。当类路径中存在多种实现时,系统优先选择首个成功实例化的存储作为默认项;若未显式指定,默认策略取决于 Vert.x 是否处于集群模式:若在集群环境下则默认采用 ClusteredSessionStore,否则默认采用本地内存存储 LocalSessionStore。
本地内存会话存储 (Local session store)
该实现将全部会话数据保存在本地 JVM 内存中,仅供当前 Vert.x 节点访问。
当你的应用仅部署为单实例,或者你在前端负载均衡层配置了 Sticky Session(粘性会话),确保来自同一客户端浏览器的所有 HTTP 请求始终被转发到同一个 Vert.x 节点时,该存储非常适用。
如果你无法确保请求始终落入同一实例,则切勿使用本地存储,否则请求轮询到其他服务器节点时会因找不到会话而导致登录凭据丢失。
本地会话存储基于 Vert.x SharedData 的本地共享 Map 实现,并内置了一个过期回收清理器(Reaper)。
清理间隔时间可以通过 JSON 格式配置项 reaperInterval 进行自定义。
创建本地 SessionStore 示例:
SessionStore store1 = LocalSessionStore.create(vertx);
// 创建本地会话存储,并指定使用的本地共享 Map 名称。
// 这在同一个 Vert.x 实例内运行多个独立应用且需要隔离各自会话时非常有用
SessionStore store2 = LocalSessionStore.create(
vertx,
"myapp3.sessionmap");
// 创建本地会话存储,指定 Map 名称并将过期会话扫描清理周期设为 10 秒
SessionStore store3 = LocalSessionStore.create(
vertx,
"myapp3.sessionmap",
10000);
分布式集群会话存储 (Clustered session store)
在该实现中,所有会话数据保存在跨 Vert.x 集群共享的分布式内存 Map 中。
如果你没有开启粘性会话(即前端负载均衡器将同一客户端的请求随机轮询分发到不同集群节点),该实现是官方推荐的最佳方案。
集群内的任意节点均可随时透明存取同一个客户端的 Session 数据。
使用集群会话存储前,请确保 Vert.x 实例已成功以集群模式(Clustered Mode)启动。
创建集群 SessionStore 示例:
Vertx.clusteredVertx(new VertxOptions())
.onSuccess(vertx -> {
// 使用默认配置创建集群会话存储
SessionStore store1 = ClusteredSessionStore.create(vertx);
// 创建集群会话存储并指定集群分布式 Map 的名称
SessionStore store2 = ClusteredSessionStore.create(
vertx,
"myclusteredapp3.sessionmap");
});
其他会话存储后端 (Other stores)
此外,生态中还提供了多种开箱即用的扩展存储后端,只需在工程中引入对应的 Maven 依赖即可:
-
Cookie Session Store:优势在于服务端无需维护任何状态,所有会话数据经由 强加密 后直接下发并保存在客户端 Cookie 中,服务器重启或宕机完全不会丢失会话。但是,由于数据存储在客户端,如果涉及极其敏感的核心机密数据,不建议使用此模式。
-
Redis Session Store:利用外部独立的 Redis 高性能键值数据库实现会话集中管理,适合大规模云原生无状态服务。
-
Infinispan Session Store:基于 Red Hat Infinispan 企业级分布式数据网格。
各模块的 Maven 坐标为:
-
groupId:io.vertx -
artifactId:vertx-web-sstore-{cookie|redis|infinispan|caffeine}
Infinispan Web Session 存储
基于 Infinispan Java 客户端实现的 SessionStore。
快速入门
要使用本模块,请在 Maven POM 文件的 dependencies 配置节中添加以下内容:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-sstore-infinispan</artifactId>
<version>5.1.10</version>
</dependency>
若使用 Gradle:
compile 'io.vertx:vertx-web-sstore-infinispan:5.1.10'
基本使用
如果此 Session 存储是依赖项中唯一的实现,你可以通过通用方式初始化它:
JsonObject config = new JsonObject()
.put("servers", new JsonArray()
.add(new JsonObject()
.put("host", "server1.datagrid.mycorp.int")
.put("username", "foo")
.put("password", "bar"))
.add(new JsonObject()
.put("host", "server2.datagrid.mycorp.int")
.put("username", "foo")
.put("password", "bar"))
);
SessionStore store = SessionStore.create(vertx, config);
SessionHandler sessionHandler = SessionHandler.create(store);
router.route().handler(sessionHandler);
否则,请显式使用 InfinispanSessionStore 类型:
JsonObject config = new JsonObject()
.put("servers", new JsonArray()
.add(new JsonObject()
.put("host", "server1.datagrid.mycorp.int")
.put("username", "foo")
.put("password", "bar"))
.add(new JsonObject()
.put("host", "server2.datagrid.mycorp.int")
.put("username", "foo")
.put("password", "bar"))
);
InfinispanSessionStore store = InfinispanSessionStore.create(vertx, config);
SessionHandler sessionHandler = SessionHandler.create(store);
router.route().handler(sessionHandler);
配置
根级配置项包括:
-
servers:必填,服务器定义的 JSON 数组(见下文) -
cacheName:可选,用于存储会话数据的缓存名称(默认值为vertx-web.sessions) -
retryTimeout:可选,Session 处理器从存储检索值时的重试超时时间,单位为毫秒(默认值为5000)
单个服务器定义对象的配置项包括:
-
uri:可选,https://infinispan.org/blog/2020/05/26/hotrod-uri/[Hot Rod URI] -
host:可选(默认值为localhost) -
port:可选(默认值为11222) -
clientIntelligence:可选(取值为BASIC、TOPOLOGY_AWARE或HASH_DISTRIBUTION_AWARE之一) -
username:必填 -
password:必填 -
realm:可选(默认值为default) -
saslMechanism:可选(默认值为DIGEST-MD5) -
saslQop:可选(取值为AUTH、AUTH_INT或AUTH_CONF之一)
若配置了 uri 项,则其它配置项将被忽略。
|
如有高级配置需求,你可以提供自定义的 RemoteCacheManager:
InfinispanSessionStore sessionStore = InfinispanSessionStore.create(vertx, config, remoteCacheManager);
Caffeine 会话存储 (Caffeine session store)
Caffeine 会话存储基于知名的 Java 高性能本地缓存库 Caffeine 构建。 它具备业界领先的命中率与高并发吞吐能力,提供了极为高效的会话存取与基于 W-TinyLFU 的缓存淘汰策略。 该存储非常适合高吞吐且无需跨节点共享 Session 的超大规模单节点应用。
Maven 依赖引入方式:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-sstore-caffeine</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-sstore-caffeine:5.1.10'
}
创建会话处理器 (Creating the session handler)
创建好 SessionStore 之后,即可实例化 SessionHandler 并将其挂载到路由上。务必确保该处理器优先于所有业务 Handler 执行。
示例:
Router router = Router.router(vertx);
// 使用默认配置创建集群会话存储
SessionStore store = ClusteredSessionStore.create(vertx);
SessionHandler sessionHandler = SessionHandler.create(store);
// SessionHandler 统一管理 Session Cookie 的下发规则,
// 例如在此处配置严格的 SameSite 安全策略:
sessionHandler.setCookieSameSite(CookieSameSite.STRICT);
// 确保所有请求优先经过 SessionHandler 处理
router.route().handler(sessionHandler);
// 挂载后续的业务路由处理器
router.route("/somepath/blah/").handler(ctx -> {
Session session = ctx.session();
session.put("foo", "bar");
// 业务处理...
});
SessionHandler 会在请求到达业务 Handler 之前,自动从 SessionStore 中检索当前请求对应的会话(若尚无会话则自动创建新会话),并挂载到路由上下文中。
默认情况下,即便业务逻辑从未读写 Session,SessionHandler 也会向客户端下发 Session Cookie。如果你希望仅在业务代码实际读写了会话时才下发 Cookie,可以调用 sessionHandler.setLazySession(true) 开启懒加载模式。
在 Handler 中使用 Session
在业务 Handler 中,通过 session 即可获取当前会话实例。
会话键(Key)必须是纯字符串。本地存储的值可以是任意 Java 对象;而在集群存储中,值必须是 Java 基本类型、Buffer、JsonObject、JsonArray 或实现了 Serializable 的可序列化对象,以便在集群节点间网络传输。
会话读写示例:
router.route().handler(sessionHandler);
// 业务路由处理器
router.route("/somepath/blah").handler(ctx -> {
Session session = ctx.session();
// 向会话中存入数据
session.put("foo", "bar");
// 从会话中读取数据
int age = session.get("age");
// 从会话中删除指定属性
JsonObject obj = session.remove("myobj");
});
当 HTTP 响应完成之后,修改后的会话数据会自动持久化保存到底层 Store 中。
你可以使用 destroy 主动销毁当前会话。这会立刻将该会话从路由上下文和后端存储中彻底抹除。需要注意,销毁后来自该浏览器的下一次新请求经过 SessionHandler 时会自动生成一个全新的独立会话。
会话超时机制 (Session timeout)
当会话在超过指定超时周期内未被任何请求访问时,将自动判定为超时失效并从存储中清除。
每当有新请求到达并命中会话、以及响应完成会话持久化时,系统都会自动更新该会话的最后访问时间戳(Last Accessed Time)。
你也可以显式调用 setAccessed 手动标记该会话已被访问。
会话超时时间可以在创建 SessionHandler 时进行配置,默认超时时长为 30 分钟。
身份认证与授权 (Authentication / authorization)
Vert.x 提供了大量开箱即用的 Handler 用于统一管理身份认证(Authentication)与权限授权(Authorization)。在 Vert.x Web 的语境中,两者的职责明确划分如下:
-
身份认证 (Authentication):确认“当前用户是谁”
-
权限授权 (Authorization):判定“该用户被允许执行哪些操作”
其中,身份认证 通常与具体的行业协议紧密绑定,例如:
-
HTTP Basic 认证
-
HTTP Digest 认证
-
OAuth2 / OIDC 认证
-
JWT 令牌认证
-
WebAuthn / Passkey 认证
-
…
而 Vert.x 中的 权限授权 则是高度抽象和通用的,完全解耦于具体的认证协议。当然,在许多实际工程中,使用同一个安全 Provider 模块同时承载认证与授权也是非常标准且推荐的实践。
创建身份认证处理器 (Creating an Authentication handler)
要创建认证 Handler,首先需要一个 AuthenticationProvider 认证提供者实例。
认证提供者负责校验用户提交的凭证并判定合法性。Vert.x 在 vertx-auth 项目中开箱即用提供了丰富的 Provider 实现。关于各类 Provider 的详细配置指南,请查阅专门的 Auth 模块文档。
以下是使用指定 Provider 构建基础 Basic 认证处理器的简明示例:
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
AuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider);
在应用程序中集成认证拦截 (Handling authentication in your application)
假设你希望所有以 /private/ 开头的访问路径都必须先经过身份认证。你只需要将认证处理器挂载到这些业务路由之前即可:
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
AuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider);
// 所有以 '/private/' 开头的请求都会被强制拦截并进行身份认证
router.route("/private/*").handler(basicAuthHandler);
router.route("/someotherpath").handler(ctx -> {
// 公开接口路径 —— 无需登录即可直接访问
});
router.route("/private/somepath").handler(ctx -> {
// 必须登录后才能访问此处的业务逻辑
// 此时 authenticated() 的返回值将始终为 true
boolean isAuthenticated = ctx.userContext().authenticated();
});
当认证处理器成功校验用户身份后,它会将一个 User 对象注入到 UserContext 中。你可以在后续的任意 Handler 中通过 userContext 或 ctx.user() 便捷获取当前登录用户。
若希望用户对象自动缓存在 Session 会话中跨请求保持登录状态(免去每次请求重复提交凭证),务必在认证处理器之前先挂载 SessionHandler。
获取到 User 对象后,你便可以调用其内置方法执行编程式权限核验。
若需要注销退出登录,只需调用 RoutingContext 的 logout。注销操作会自动从 Session 中彻底清除当前用户,并执行重定向跳转(支持指定跳转 URL,默认为重定向回根路径 /)。
HTTP Basic 基础认证
HTTP Basic 基础认证 是一种非常轻量、通用的 HTTP 协议级认证方案,适用于对交互要求不高的内部管理接口或简易服务。
在 Basic 认证中,用户的凭证仅经过 Base64 编码后通过请求头明文跨网络传输,因此必须且只能在 HTTPS 安全加密通道下使用。
在 Basic 认证流程中,当未经认证的用户请求受保护资源时,BasicAuthHandler 会返回 401 Unauthorized 响应并附带 WWW-Authenticate 标头,提示浏览器弹出原生登录对话框。
用户输入用户名和密码后,浏览器会重新发起请求,此时请求头中已附带 Base64 编码的 Authorization: Basic … 凭据。
当 BasicAuthHandler 接收到该凭证后,会调用底层配置的 AuthenticationProvider 执行身份校验。校验通过则继续流转到后续业务路由;若校验失败则直接响应 403 Forbidden 拒绝访问。
基于重定向的表单登录认证 (Redirect authentication handler)
在典型的 Web 交互网站中,当未登录用户尝试访问受保护页面时,通常希望自动将其重定向到登录表单页。
用户在登录页填写表单并提交,服务端验证通过后,再自动将其重定向回最初尝试访问的原始目标页面。
要实现这种重定向认证流程,只需使用 RedirectAuthHandler 代替 Basic 认证处理器。
此外,你还需要配置展示登录页面的路由以及处理登录表单 POST 提交的处理器。Vert.x Web 提供了开箱即用的 FormLoginHandler 专门负责处理表单提交。
FormLoginHandler 必须依赖 BodyHandler 才能正常读取并解析 POST 表单内容。
|
使用重定向认证处理器的完整应用骨架(默认登录跳转路径为 /loginpage):
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
// 所有以 '/private/' 开头的受限路径均由 RedirectAuthHandler 保护
router
.route("/private/*")
.handler(RedirectAuthHandler.create(authProvider));
// 处理登录表单提交:必须挂载 BodyHandler 以解析 POST 表单参数
router.post("/login")
.handler(BodyHandler.create())
.handler(FormLoginHandler.create(authProvider));
// 挂载静态资源处理器,用于托管 HTML 登录表单页及静态样式
router.route().handler(StaticHandler.create());
router
.route("/someotherpath")
.handler(ctx -> {
// 公开资源路径 —— 允许匿名直接访问
});
router
.route("/private/somepath")
.handler(ctx -> {
// 受保护的业务逻辑
// 成功进入此处的请求已确保完成身份验证
boolean isAuthenticated = ctx.userContext().authenticated();
});
JWT 令牌认证
使用 JWT 身份认证,可以通过权限来保护资源,未获充分授权的用户将被拒绝访问。你需要添加 io.vertx:vertx-auth-jwt:5.1.10 依赖来使用 JWTAuthProvider:
使用 JWT 认证主要分为两个环节:
-
搭建用于签发 Token 的接口(或直接对接第三方 OAuth2/IdP 认证授权服务中心)
-
在受保护路由上配置 JWTAuthHandler 用于拦截并校验请求中的 Token
请注意:所有涉及 Token 传输与签发的接口都必须且只能在 HTTPS 下提供服务,否则在网络链路中 Token 极易被窃听抓包从而遭受会话劫持攻击。
签发 Token 示例:
Router router = Router.router(vertx);
JWTAuthOptions authConfig = new JWTAuthOptions()
.setKeyStore(new KeyStoreOptions()
.setType("jceks")
.setPath("keystore.jceks")
.setPassword("secret"));
JWTAuth jwt = 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(jwt.generateToken(new JsonObject().put("sub", "paulo")));
} else {
ctx.fail(401);
}
});
客户端在获取到该 Token 之后,在后续的所有请求中,只需在 HTTP 请求头中携带 Authorization: Bearer <token> 即可:
Router router = Router.router(vertx);
JWTAuthOptions authConfig = new JWTAuthOptions()
.setKeyStore(new KeyStoreOptions()
.setType("jceks")
.setPath("keystore.jceks")
.setPassword("secret"));
JWTAuth authProvider = JWTAuth.create(vertx, authConfig);
// 对 /protected/* 路径启用 JWT 校验
router.route("/protected/*").handler(JWTAuthHandler.create(authProvider));
router.route("/protected/somepage").handler(ctx -> {
// 受保护的业务逻辑...
});
JWT 允许你在签发阶段将任意自定义业务 Claims(声明)打包写入 Payload。由于令牌本身具备防篡改自包含特性,服务端完全无需存储会话状态即可完成鉴权,使得应用能以极致的无状态水平伸缩扩展。在签发时只需向 JsonObject 中注入键值对即可:
JWTAuthOptions authConfig = new JWTAuthOptions()
.setKeyStore(new KeyStoreOptions()
.setType("jceks")
.setPath("keystore.jceks")
.setPassword("secret"));
JWTAuth authProvider = JWTAuth.create(vertx, authConfig);
authProvider
.generateToken(
new JsonObject()
.put("sub", "paulo")
.put("someKey", "some value"),
new JWTOptions());
在后续 Handler 中消费读取注入的 Claims:
Handler<RoutingContext> handler = ctx -> {
String theSubject = ctx.user().principal().getString("sub");
String someKey = ctx.user().principal().getString("someKey");
};
配置权限授权规则 (Configuring authorization)
前面的章节主要聚焦于认证(确认身份)。而授权则是确认“该用户具备哪些权限”。
认证机制往往因协议而异,而 授权 (Authorization) 则是协议无关的,所有鉴权信息均从 User 对象中提取。
在进行权限核验前,必须先将授权信息加载至该 User 对象中。为此需要使用 AuthorizationHandler。授权处理器会从指定的 AuthorizationProvider 中动态查询并加载用户权限。
router.route().handler(
// 创建负责执行权限断言核验的 AuthorizationHandler
AuthorizationHandler.create(
// 声明必须满足的权限断言
PermissionBasedAuthorization.create("can-do-work"))
// 指定从哪个授权提供者中反查用户的权限集
.addAuthorizationProvider(authProvider));
权限查询支持同时聚合多个数据源,只需链式多次调用 addAuthorizationProvider(provider) 即可。
以下示例展示了为系统中不同路径配置差异化访问权限的典型模式(底层 Provider 支持基于角色模型、基于权限字符串模型等多种机制):
router.route("/listproducts/*").handler(
AuthorizationHandler.create(
// 要求具备 list_products 操作权限
PermissionBasedAuthorization.create("list_products"))
.addAuthorizationProvider(authProvider));
// 仅拥有 "admin" 角色的超级管理员允许访问 /private/settings
router.route("/private/settings/*").handler(
AuthorizationHandler.create(
// 要求具备 admin 角色
RoleBasedAuthorization.create("admin"))
.addAuthorizationProvider(authProvider));
链式组合多重认证处理器 (Chaining multiple authentication handlers)
在许多实际生产应用中,同一个接口往往需要同时支持多种认证通道(例如既支持前台用户的 Cookie/表单重定向,又支持外部系统以 HTTP Basic 或 JWT Bearer 令牌调用)。为此 Vert.x 提供了强大的 ChainAuthHandler(责任链式认证处理器)。
需要注意,不同类型的 Handler 对 Provider 的具体要求有所不同:
-
JWTAuthHandler要求使用JWTAuth
而以下常用处理器则具有普适兼容性,可以接收任意通用的 Provider:
假设你希望同时支持 HTTP Basic 认证 与 表单重定向登录,你可以这样组装责任链:
ChainAuthHandler chain = ChainAuthHandler.any();
// 向链中添加 Basic 认证处理器
chain.add(BasicAuthHandler.create(provider));
// 向链中追加表单重定向认证处理器
chain.add(RedirectAuthHandler.create(provider));
// 为路由挂载认证链
router.route("/secure/resource").handler(chain);
// 挂载核心业务处理器
router.route("/secure/resource").handler(ctx -> {
// 业务处理...
});
当客户端发来不带任何凭证请求头的访问时,责任链首先执行 BasicAuthHandler 发现无法认证,随后自动交由 RedirectAuthHandler 处理,直接触发重定向将用户引导至登录表单页。
认证链执行同样遵循严格的物理添加顺序。如果你希望在没有凭证时优先弹出浏览器原生的 HTTP Basic 密码框,只需调换两者的 add 顺序即可。
当客户端显式提供了 Authorization: Basic … 请求头时,BasicAuthHandler 会立即进行校验,一旦验证成功责任链便会短路终止并继续向下流转业务;只有当凭证无效(如密码错误)时,链条才会继续向后流转给重定向处理器。
责任链还支持更为复杂的布尔组合逻辑,例如表达:HandlerA 或(HandlerB 且 HandlerC):
ChainAuthHandler chain =
ChainAuthHandler.any()
.add(authNHandlerA)
.add(ChainAuthHandler.all()
.add(authNHandlerB)
.add(authNHandlerC));
router.route("/secure/resource").handler(chain);
router.route("/secure/resource").handler(ctx -> {
// 业务处理...
});
用户身份模拟与切换 (Impersonation)
在使用身份认证时,有时用户的身份需要临时切换升级。例如普通客服人员需要临时以特定客户或 admin 权限身份执行敏感审核操作。这可以通过以下 API 达成:
router
.route("/high/security/route/check")
.handler(ctx -> {
// 若当前用户非管理员,提示其临时提升为管理员身份
ctx
.userContext()
.loginHint("admin")
.impersonate();
});
当高权限操作完成后,可调用还原方法恢复原有身份:
router
.route("/high/security/route/back/to/me")
.handler(ctx -> {
ctx
.userContext()
.restore();
});
该模拟切换流程完全在内部上下文流转完成,无需调用其他外部路由接口,从而杜绝了从外部直接伪造身份切换的安全漏洞。
刷新与重新确认身份 (Refreshing identities)
在执行核心高风险操作(如大额转账、修改安全密码)时,系统往往需要确认用户本人当前依然在场并重新验证。这可以通过刷新用户身份来实现:
router
.route("/high/security/route/refresh/me")
.handler(ctx -> {
ctx
.userContext()
.refresh();
});
此操作会强制要求用户重新提交凭证完成认证,以彻底确认操作者身份。
安全事件审计 (Security Auditing)
Vert.x-Web 内置了专业的安全审计日志处理器 SecurityAuditLoggerHandler。该处理器对于将关键安全事件结构化输出并同步对接到外部 EDR/XDR 等企业级安全终端威胁感知与响应平台极为有用。作为顶层全局处理器配置方式如下:
router.route()
.handler(SecurityAuditLoggerHandler.create());
事件类型 (Event types)
该处理器会自动采集并记录以下维度的核心安全事件:
REQUEST 请求事件
每当收到 HTTP 请求时(无论后续执行成功或失败),均会输出一条请求日志:
[REQUEST epoch="1679389193762" source="127.0.0.1" destination="127.0.0.1" resource="HTTP/1.1 GET /protected/foo" status=200] OK
若请求流转发生失败异常:
[REQUEST epoch="1679389193924" source="127.0.0.1" destination="127.0.0.1" resource="HTTP/1.1 GET /somedir" status=500] FAIL
日志中记录的核心指标字段包括:
-
epoch:自 Unix 纪元以来的时间戳(毫秒) -
source:源客户端 IP 地址 -
destination:目标接收 IP 地址 -
resource:被请求的 HTTP 协议版本与 URI 资源路径 -
status:返回的 HTTP 响应状态码
AUTHENTICATION 认证事件
当请求完成身份认证时记录:
[AUTHENTICATION epoch="1679389193762" source="127.0.0.1" destination="127.0.0.1" resource="HTTP/1.1 GET /protected/foo" token="********************************..."] OK
审计处理器具备内置的数据脱敏机制,会自动对密码、Token 密钥等敏感隐私字段进行星号遮罩脱敏。若认证失败也会记录失败审计:
[AUTHENTICATION epoch="1679389193568" source="127.0.0.1" destination="127.0.0.1" resource="HTTP/1.1 GET /protected/foo" token="********************************..."] FAIL
AUTHORIZATION 授权事件
当用户成功通过权限核验时记录:
[AUTHORIZATION epoch="1679389193678" source="127.0.0.1" destination="127.0.0.1" resource="HTTP/1.1 GET /protected/page1" subject="paulo" authorization="ROLE[role3]"] OK
授权不满足而被拒绝时同样记录:
[AUTHORIZATION epoch="1679389193678" source="127.0.0.1" destination="127.0.0.1" resource="HTTP/1.1 GET /protected/page1" subject="paulo" authorization="ROLE[role3]"] FAIL
该事件附加的关键指标包括:
-
subject:通过认证的用户主体标识 -
authorization:访问该资源*所要求必须具备*的权限/角色声明
为了防止泄露敏感的 PII(个人身份信息)或 PHI 数据,审计日志不会输出用户全量私有属性。
格式配置 (Configuration)
该处理器默认采用 RFC 5424 标准日志格式(即通用的 Syslog 格式)。你可以通过 Java 启动系统属性将其配置为单行 JSON 格式,以便于各类日志收集器(如 Fluentd、Logstash、Vector)进行结构化采集:
io.vertx.ext.auth.audit.format=rfc5424 // 或者配置为单行 JSON 输出 io.vertx.ext.auth.audit.format=json
静态资源服务 (Serving static resources)
Vert.x-Web 内置了开箱即用的静态 Web 资源托管处理器,帮助你轻松搭建高性能的静态 Web 服务器。
若需要托管 .html、.css、.js、图片或任何其他静态文件,只需使用 StaticHandler 实例。
发往静态处理器所绑定路径的请求,均会自动从本地文件系统目录或类路径(Classpath)中读取并提供文件。默认的静态资源根目录为 webroot,支持自由配置。
在以下示例中,所有以 /static/ 开头的访问请求都将由 webroot 目录提供服务:
router.route("/static/*").handler(StaticHandler.create());
例如,发往 /static/css/mystyles.css 的请求,静态处理器会在文件系统目录 webroot/css/mystyle.css 中查找对应文件。
同时,它也会在 Classpath 类路径中查找名为 webroot/css/mystyle.css 的资源。这意味着你可以将所有静态前端资产直接打包进 JAR 文件(或 Fatjar)中统一分发部署。
当 Vert.x 首次在类路径中读取到某资源时,会自动将其解压并缓存到本地磁盘的临时目录中,从而避免每次请求重复进行耗时的解压操作。
该处理器原生支持 HTTP Range 断点续传与范围请求。当客户端请求静态资源时,处理器会在响应头中返回 Accept-Ranges: bytes 声明支持范围传输。客户端后续携带 Range: bytes=start-end 的请求将收到带有标准 Content-Range 头的 206 Partial Content 部分分块响应。
配置客户端缓存 (Configuring caching)
默认情况下,静态处理器会自动设置一系列标准的 HTTP 缓存控制头,以便浏览器能够高效利用本地缓存。
Vert.x-Web 会自动协商并注入 Cache-Control、Last-Modified 以及 Date 响应头。
Cache-Control 默认配置为 max-age=86400(即有效缓存 1 天)。如有需要,你可以通过 setMaxAgeSeconds 自定义缓存秒数。
当浏览器发起带有 If-Modified-Since 请求头的 GET 或 HEAD 请求时,若服务端文件自该时间点后未发生修改,处理器会直接返回 304 Not Modified 状态码,通知浏览器直接使用其本地已缓存的资源。
如果不希望自动下发缓存控制标头,可以通过 setCachingEnabled 显式关闭。
开启缓存处理后,Vert.x-Web 会在 JVM 内存中缓存各个资源的最后修改时间戳(Last Modified Date),从而避免每次请求都进行磁盘 I/O 检查。
内存中的缓存项拥有过期时间,一旦过期,系统会重新检查磁盘文件并刷新内存缓存。
如果你明确知道磁盘上的文件在服务运行期间绝不会改变,那么内存缓存项实际上永远不会失效(这也是默认行为)。
若在开发或热更新环境中文件可能会发生变动,你可以通过 setFilesReadOnly 将只读模式置为 false。
若需调整内存中允许缓存的条目上限,使用 setMaxCacheSize。
若需调整内存缓存项的过期刷新周期,使用 setCacheEntryTimeout。
默认首页配置 (Configuring the index page)
对根目录 / 的请求会自动指向默认首页。默认首页文件名是 index.html。
你可以通过 setIndexPage 修改默认首页名称。
修改 Web 根目录 (Changing the web root)
默认静态资源存放在 webroot 目录下。如需自定义目录路径,可通过 StaticHandler.create 指定。
隐藏文件服务 (Serving hidden files)
默认情况下,静态处理器允许访问隐藏文件(即以 . 开头的文件)。
如果你希望禁止外部访问隐藏文件,可以通过 setIncludeHidden 将其关闭。
目录浏览列表 (Directory listing)
静态处理器还支持像传统 Web 服务器一样列出目录下的文件列表。该特性默认关闭。你可以通过 setDirectoryListing 将其开启。
开启目录列表后,返回的格式取决于客户端请求的 Accept 标头。
对于 text/html 网页浏览格式,用于渲染目录索引列表的 HTML 模板可以通过 setDirectoryTemplate 自定义。
禁用磁盘临时文件缓存 (Disabling file caching on disk)
默认情况下,Vert.x 会将从 Classpath 类路径中读取到的静态资源解压并缓存在当前工作目录下 .vertx 的子目录中。在生产环境下将服务打包为 Fatjar 运行时,这能极大避免每次从 JAR 包解压的高昂开销。
但在本地开发调试阶段,这可能会引发困扰:如果你在服务运行期间修改了前端静态文件,由于本地磁盘缓存的存在,浏览器刷新时看到的仍然是旧文件。
要关闭该磁盘缓存,可在启动时将 Vert.x 选项中的 fileResolverCachingEnabled 属性置为 false,或者在 IDE 运行配置中传入 Java 系统属性 -Dvertx.disableFileCaching=true。
跨域资源共享处理 (CORS handling)
跨域资源共享 (CORS) 是一种让浏览器安全地允许一个域的 Web 应用去请求另一个域资源的 W3C 标准机制。
Vert.x-Web 提供了 CorsHandler,为你自动处理复杂的 CORS 预检(Preflight)与协议响应头。
示例:
router.route()
.handler(
CorsHandler.create()
.addOriginWithRegex("vertx\\.io")
.allowedMethod(HttpMethod.GET));
router.route().handler(ctx -> {
// 具体的业务处理器
});
多租户支持 (Multi Tenant)
在 SaaS 架构中,一套应用系统通常需要同时服务多个不同的企业租户。Vert.x-Web 提供了 MultiTenantHandler 帮助你轻松构建多租户分流体系。
如果租户标识是通过特定的 HTTP 请求头(例如 X-Tenant)传递的,创建处理器非常简单:
router.route().handler(MultiTenantHandler.create("X-Tenant"));
随后你可以为不同的租户分别绑定特定的业务 Handler:
MultiTenantHandler.create("X-Tenant")
.addTenantHandler("tenant-A", ctx -> {
// 针对租户 A 的业务逻辑...
})
.addTenantHandler("tenant-B", ctx -> {
// 针对租户 B 的业务逻辑...
})
// 可选:兜底默认处理器
.addDefaultHandler(ctx -> {
// 当未匹配到任何受支持租户时的处理...
});
这在多租户安全鉴权场景下非常实用:
OAuth2Auth gitHubAuthProvider = GithubAuth
.create(vertx, "CLIENT_ID", "CLIENT_SECRET");
// 为 GitHub 认证在运行中的服务上创建 OAuth2 处理器,
// 第二个参数为回调全路径
OAuth2AuthHandler githubOAuth2 = OAuth2AuthHandler.create(
vertx,
gitHubAuthProvider,
"https://myserver.com/github-callback");
// 注册接收 GitHub 认证回调的端点
githubOAuth2.setupCallback(router.route("/github-callback"));
// 创建针对 Google 的 OAuth2 Provider
OAuth2Auth googleAuthProvider = OAuth2Auth.create(vertx, new OAuth2Options()
.setClientId("CLIENT_ID")
.setClientSecret("CLIENT_SECRET")
.setSite("https://accounts.google.com")
.setTokenPath("https://www.googleapis.com/oauth2/v3/token")
.setAuthorizationPath("/o/oauth2/auth"));
// 为 Google 创建 OAuth2 处理器
OAuth2AuthHandler googleOAuth2 = OAuth2AuthHandler.create(
vertx,
googleAuthProvider,
"https://myserver.com/google-callback");
// 注册接收 Google 回调的端点
googleOAuth2.setupCallback(router.route("/google-callback"));
// 根据租户分发到不同的安全认证中心
router.route().handler(
MultiTenantHandler.create("X-Tenant")
// 使用 GitHub 租户的用户走此通道:
.addTenantHandler("github", githubOAuth2)
// 使用 Google 租户的用户走此通道:
.addTenantHandler("google", googleOAuth2)
// 其他未授权租户直接响应 401
.addDefaultHandler(ctx -> ctx.fail(401)));
在后续的任意 Handler 中,均可随时从 RoutingContext 中获取解析出的租户标识,例如用于动态决定连接哪个租户的数据库或加载何种租户皮肤:
router.route().handler(ctx -> {
// 默认 Key 为 MultiTenantHandler.TENANT 中定义的 "tenant",
// 该键名可在工厂方法创建时自由指定
String tenant = ctx.get(MultiTenantHandler.TENANT);
switch (tenant) {
case "google":
// 为 Google 租户加载专属资源
break;
case "github":
// 为 GitHub 租户加载专属资源
break;
}
});
多租户处理器是一个强大的分流路由积木,能够让多租户应用在同一实例内并行协作。但需要明确:它在底层不提供任何基于沙箱的硬物理隔离,应用代码本身应保证业务逻辑严密,避免跨租户状态污染。
模板引擎体系 (Templates)
Vert.x-Web 内置支持多种业界主流的模板引擎,能够以极高的非阻塞性能生成服务端动态 HTML 页面。你也可以轻松挂载自定义的模板引擎。
模板引擎通过 TemplateEngine 接口定义。使用模板引擎渲染时调用 render 方法。
在 Web 路由中最简单的方式不是手动调用模板引擎,而是使用开箱即用的 TemplateHandler。
该处理器会根据当前 HTTP 请求的路径自动定位并调用模板引擎完成渲染输出。
默认情况下,TemplateHandler 会在名为 templates 的目录中查找模板文件,该目录路径支持自由配置。
处理器渲染完成后默认设置 Content-Type: text/html 响应头,同样支持自定义。
创建 TemplateHandler 时需要传入具体模板引擎的实例。由于模板引擎未与 Vert.x-Web 强行打包捆绑,需在工程构建文件中按需引入对应依赖。
以下是使用 Handlebars 模板引擎的简易示例:
TemplateEngine engine = HandlebarsTemplateEngine.create();
TemplateHandler handler = TemplateHandler.create(engine);
// 将所有以 /dynamic/ 开头的 GET 请求分发至模板处理器
// 例如 /dynamic/graph.hbs 会在 /templates/graph.hbs 中查找并渲染模板
router.get("/dynamic/*").handler(handler);
// 或者匹配所有以 .hbs 结尾的资源请求
router.getWithRegex(".+\\.hbs").handler(handler);
MVEL 模板引擎
使用 MVEL 模板引擎需引入以下依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-mvel</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-mvel:5.1.10'
}
通过 io.vertx.ext.web.templ.mvel.MVELTemplateEngine#create(io.vertx.core.Vertx) 创建引擎实例。
若文件名未显式指定扩展名,MVEL 引擎默认查找 .templ 后缀的模板文件。
路由上下文数据 data 会作为变量源自动注入 MVEL 模板中供表达式调用。若模板需要读取 Request、Response 或 Session 数据,需先将其放入 context data 中。
模板语法示例:
来自路由上下文 data 中的 bar 变量值为 @{bar}。
|
只要模板存放在本地文件系统中,即可在 MVEL 模板中使用 但若模板是通过 Vert.x 文件解析器从 JAR Classpath 中加载的,子模板包含将不生效。这是 MVEL 引擎本身的局限(其他部分引擎支持插件化文件解析器)。 |
编写语法请参阅 MVEL 模板官方文档。
Pug (Jade) 模板引擎
引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-pug</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-pug:5.1.10'
}
通过 io.vertx.ext.web.templ.pug.PugTemplateEngine#create(io.vertx.core.Vertx) 实例化。默认查找 .pug 扩展名文件。
模板语法示例:
!!! 5
html
head
title 来自路由上下文的 bar 值为 #{bar}。
body
详情请参阅 Pug4j 文档。
Handlebars 模板引擎
引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-handlebars</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-handlebars:5.1.10'
}
通过 io.vertx.ext.web.templ.handlebars.HandlebarsTemplateEngine#create(io.vertx.core.Vertx) 创建实例。默认查找 .hbs 模板。
|
Handlebars 模板出于安全性考虑默认不允许任意反射调用 Java 对象方法。 如果需要访问请求路径、请求参数或 Session 会话数据,应在前置 Handler 中显式存入 Context Data:
|
语法请参考 Handlebars.java 文档。
Thymeleaf 模板引擎
引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-thymeleaf</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-thymeleaf:5.1.10'
}
通过 io.vertx.ext.web.templ.thymeleaf.ThymeleafTemplateEngine#create(io.vertx.core.Vertx) 实例化。默认查找 .html 扩展名。
语法示例:
<div th:fragment="example"><p th:text="${foo}"></p>
<p th:text="${bar}"></p></div>
语法请参考 Thymeleaf 官方指南。
Apache FreeMarker 模板引擎
引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-freemarker</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-freemarker:5.1.10'
}
通过 io.vertx.ext.web.templ.Engine#create() 实例化。默认查找 .ftl 扩展名。
语法示例:
你好,${foo} 和 ${bar}!
语法参考 Apache FreeMarker 手册。
Pebble 模板引擎
引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-pebble</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-pebble:5.1.10'
}
通过 io.vertx.ext.web.templ.pebble.PebbleTemplateEngine#create(vertx) 实例化。默认查找 .peb 文件。
语法示例:
Hello {{foo}} and {{bar}}
语法参考 Pebble 官方文档。
Rocker 模板引擎
Rocker 是针对 Java 优化的高性能编译期强类型模板引擎。引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-rocker</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-rocker:5.1.10'
}
通过 io.vertx.ext.web.templ.rocker.RockerTemplateEngine#create() 实例化。
语法示例:
@import io.vertx.core.json.JsonObject @args (String foo, String bar) Hello @foo and @bar
HTTL 模板引擎
引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-httl</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-httl:5.1.10'
}
通过 io.vertx.ext.web.templ.httl.HTTLTemplateEngine#create(io.vertx.core.Vertx) 实例化。默认扩展名为 .httl。
语法示例:
<!-- #set(String foo, String bar) -->
Hello ${foo} and ${bar}
Rythm 模板引擎
引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-rythm</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-rythm:5.1.10'
}
通过 io.vertx.ext.web.templ.rythm.RythmTemplateEngine#create(io.vertx.core.Vertx) 实例化。默认扩展名 .html。
语法示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>标题</title> </head> <body> <p>@foo</p> <p>@bar</p> </body> </html>
JTE 模板引擎
JTE(Java Template Engine)是专为现代化轻量 Java 打造的高速类型安全模板。引入依赖:
-
Maven(在
pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-templ-jte</artifactId>
<version>5.1.10</version>
</dependency>
-
Gradle(在
build.gradle文件中):
dependencies {
compile 'io.vertx:vertx-web-templ-jte:5.1.10'
}
通过 io.vertx.ext.web.templ.jte.JteTemplateEngine#create(io.vertx.core.Vertx, java.lang.String) 实例化。
语法示例:
@import io.vertx.core.json.JsonObject
@param String foo
@param String bar
Hello ${foo} and ${bar}
模板编译缓存 (Caching)
多数模板引擎均内置了对编译期 AST/字节码的缓存能力。缓存统一保存在 Vert.x SharedData 的本地共享 Map 中,使同实例下的各个 Verticle 能安全高效地复用模板缓存。
禁用模板缓存
在本地开发调试阶段,你通常希望实时修改模板实时生效,无需重启服务。
为此只需设置系统属性 -Dvertxweb.environment=dev 或环境变量 VERTXWEB_ENVIRONMENT=dev。默认情况下,生产环境始终开启缓存加速。
错误页处理器 (Error handler)
虽然你可以通过 TemplateHandler 自行渲染个性化错误页,但 Vert.x-Web 同样内置了一个精美美观的开箱即用错误展示处理器 ErrorHandler。
直接作为 failureHandler 挂载到指定路由即可生效。
请求访问日志 (Request logger)
Vert.x-Web 提供了 LoggerHandler 用于记录 HTTP 访问日志。请务必将该处理器挂载在可能触发异常的业务 Handler 之前。
默认记录到 Vert.x 内部日志框架(可无缝桥接至 SLF4J、Log4j2 或 JUL)。详见 LoggerFormat。
Favicon 图标托管 (Serving favicons)
Vert.x-Web 内置了专门服务网站 Favicon 的 FaviconHandler。
支持指定文件系统绝对路径,或者默认直接从 Classpath 中自动读取名为 favicon.ico 的文件。
超时熔断处理器 (Timeout handler)
Vert.x-Web 提供了请求超时控制处理器 TimeoutHandler。
若业务处理超过设定耗时未输出响应,系统将主动中断并向客户端返回 503 Service Unavailable。
为以 /foo 开头的请求设置 5 秒超时保护:
router.route("/foo/").handler(TimeoutHandler.create(5000));
响应耗时统计 (Response time handler)
该处理器会在响应头中自动追加 x-response-time 标头,精准记录从请求接收到响应写出经历的毫秒耗时,例如:x-response-time: 1456ms。
Content-Type 自动推导处理器 (Content type handler)
ResponseContentTypeHandler 能够根据内容协商自动为响应补齐 Content-Type 头,避免在每一个业务 Handler 中编写冗余的 putHeader("Content-Type", …) 样板代码:
router.route("/api/*").handler(ResponseContentTypeHandler.create());
router
.get("/api/books")
.produces("application/json")
.handler(ctx -> findBooks()
.onSuccess(books -> ctx.response().end(toJson(books)))
.onFailure(ctx::fail));
SockJS 协议与实时长连接 (SockJS)
SockJS 是一个高兼容性的客户端 JavaScript 库与全双工长连接传输协议。它提供类似于 WebSocket 的抽象接口,但即便在不支持原生 WebSocket 的陈旧浏览器或代理网络中,也能自动降级采用 HTTP 轮询、长轮询或流式传输平替,做到开箱即用的透明通信。
详情请参考 SockJS 官方网站。
SockJS 处理器 (SockJS handler)
Vert.x-Web 提供了 SockJSHandler。
你可以通过 SockJSHandlerOptions 配置心跳周期等参数:
Router router = Router.router(vertx);
SockJSHandlerOptions options = new SockJSHandlerOptions()
.setHeartbeatInterval(2000);
SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options);
处理 SockJS 套接字 (Handling SockJS sockets)
在服务端为 SockJSHandler 绑定套接字处理器,每当有客户端成功建立 SockJS 连接时该 Handler 就会被触发:
传入的对象是 SockJSSocket,它同时实现了 ReadStream 与 WriteStream 响应式流式接口,操作体验与原生的 NetSocket 或 WebSocket 保持高度一致。
简单的 SockJS 数据回声(Echo)示例:
Router router = Router.router(vertx);
SockJSHandlerOptions options = new SockJSHandlerOptions()
.setHeartbeatInterval(2000);
SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options);
// 当原生 WebSocket 不可用(降级走 HTTP POST)时,需要 BodyHandler 支持读取数据
router.post("/myapp/*").handler(BodyHandler.create());
router.route("/myapp/*")
.subRouter(sockJSHandler.socketHandler(sockJSSocket -> {
// 将收到的数据原样写回客户端
sockJSSocket.handler(sockJSSocket::write);
}));
客户端 JavaScript 接入 (The client side)
在前端可直接引入 NPM 包 sockjs-client 或通过 CDN 加载:
<script src="https://unpkg.io/sockjs-client@1.5.0/dist/sockjs.min.js"></script>
客户端连接核心逻辑:
var sock = new SockJS('http://mydomain.com/myapp');
sock.onopen = function() {
console.log('已连接');
};
sock.onmessage = function(e) {
console.log('收到消息', e.data);
};
sock.onclose = function() {
console.log('连接关闭');
};
sock.send('测试数据');
配置 SockJS 处理器
可通过 SockJSHandlerOptions 进行深度参数调优。
默认情况下配置未指定 Origin。为了防范跨站 WebSocket 劫持攻击(CSWSH),强烈建议配置 Origin 属性严格校验来源域名。
|
通过 EventBus 向 SockJS 套接字发送数据
每个 SockJSSocket 均可在 EventBus 上注册事件接收地址,地址通过 sockJSSocket.writeHandlerID() 获取。
开启此特性后,服务端的任何其他 Verticle 只需通过 EventBus 发送消息,即可直接精准推送到客户端对应的 SockJS 长连接中:
eventBus.send(writeHandlerID, Buffer.buffer("推送给客户端的消息"));
SockJS EventBus 前端桥接 (SockJS event bus bridge)
Vert.x-Web 最令人称道的杀手级特性之一就是 EventBus 前端网桥。它将运行在服务端的 Vert.x 分布式事件总线,透明延伸扩展到了运行在前端浏览器中的 JavaScript。
通过前端客户端库 vertx-eventbus.js,浏览器端能够拥有与 Java 服务端几乎完全一致的 EventBus API —— 可以在浏览器直接向某个地址 send、publish,或 registerHandler 监听推送!
服务端只需在 SockJS 处理器上激活桥接网关:
Router router = Router.router(vertx);
SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
SockJSBridgeOptions options = new SockJSBridgeOptions();
// 将网关挂载到 /eventbus/* 路由
router
.route("/eventbus/*")
.subRouter(sockJSHandler.bridge(options));
前端 HTML/JS 极简示例:
<script src="https://unpkg.io/sockjs-client@1.5.0/dist/sockjs.min.js"></script>
<script src='https://unpkg.io/@vertx/eventbus-bridge-client.js@1.0.0-1/vertx-eventbus.js'></script>
<script>
var eb = new EventBus('http://localhost:8080/eventbus');
eb.onopen = () => {
// 注册地址监听:直接接收服务端或广播推送过来的消息
eb.registerHandler('some-address', (error, message) => {
console.log('收到消息: ' + JSON.stringify(message));
});
// 向指定地址发送消息
eb.send('some-address', {name: 'tim', age: 587});
}
</script>
网桥支持智能断线自动重连与指数避退(Exponential Backoff)。
网桥安全控制与权限白名单 (Securing the Bridge)
为防止恶意客户端随意向服务端任意内部地址注入或监听指令,SockJS 网桥默认采用默认全拒绝(Deny-all)的防火墙安全策略(除回复消息外一律拦截)。
你必须通过 SockJSBridgeOptions 显式声明允许入站(Inbound)与出站(Outbound)的地址白名单规则:
// 允许来自客户端的入站请求访问指定地址
PermittedOptions inbound1 = new PermittedOptions()
.setAddress("demo.orderMgr");
// 仅当消息体具备特定字段匹配时才放行
PermittedOptions inbound2 = new PermittedOptions()
.setAddress("demo.persistor")
.setMatch(new JsonObject().put("action", "find").put("collection", "albums"));
// 允许服务端向客户端推送特定地址的消息
PermittedOptions outbound1 = new PermittedOptions()
.setAddress("ticker.mystock");
// 允许推送以 "news." 开头的正则匹配地址
PermittedOptions outbound2 = new PermittedOptions()
.setAddressRegex("news\\..+");
SockJSBridgeOptions options = new SockJSBridgeOptions()
.addInboundPermitted(inbound1)
.addInboundPermitted(inbound2)
.addOutboundPermitted(outbound1)
.addOutboundPermitted(outbound2);
router.route("/eventbus/*").subRouter(sockJSHandler.bridge(options));
针对消息要求权限授权 (Requiring authorization for messages)
网桥规则还支持与 Vert.x 授权系统无缝打通,要求必须已登录且具备特定权限 Authority 的用户才允许发送或接收消息:
PermittedOptions inboundPermitted = new PermittedOptions()
.setAddress("demo.orderService")
.setRequiredAuthority("place_orders"); // 必须具备 place_orders 权限
拦截与监听网桥生命周期事件 (Handling event bus bridge events)
你可以在网桥上注册监听器,拦截诸如连接创建(SOCKET_CREATED)、连接关闭(SOCKET_CLOSED)、客户端发送(SEND)、客户端订阅(REGISTER)等所有细粒度网桥事件,并可通过 Promise 动态决定是否放行或篡改报文内容。
CSRF 跨站请求伪造防护 (CSRF Cross Site Request Forgery)
跨站请求伪造(CSRF/XSRF)是 Web 应用最常见的安全隐患之一。Vert.x-Web 提供了开箱即用的 CSRFHandler。
CSRFHandler 必须依赖 BodyHandler 来读取 POST/PUT 等非安全请求体内容。
|
CSRFHandler 会在每个 GET 请求的响应中注入一个包含唯一防伪 Token 的 Cookie,客户端在发起后续非安全请求时,必须在请求头或表单隐藏字段中将该 Token 原样提交以完成防伪验签:
<form action="/submit" method="POST">
<input type="hidden" name="X-XSRF-TOKEN" value="abracadabra">
</form>
在前后端分离 AJAX / Fetch 调用时,通常在页面全局 HTML <meta name="csrf-token" content="${X-XSRF-TOKEN}"> 中读取该令牌,并在 AJAX 请求头中携带 X-XSRF-TOKEN 即可。
HSTS 安全传输头 (HSTS Handler)
HTTP 严格传输安全(HSTS,RFC 6797)用于强制浏览器始终使用 HTTPS 安全连接与服务端通信,有效防范中间人攻击、SSL 剥离及 Cookie 劫持。挂载方式极简:
router.route().handler(HSTSHandler.create());
CSP 内容安全策略 (CSP Handler)
内容安全策略(CSP)是防御跨站脚本攻击(XSS)与数据注入攻击的行业强效盾牌,通过白名单机制限制页面可加载的脚本、样式及媒体源:
router.route().handler(
CSPHandler.create()
.addDirective("default-src", "*.trusted.com"));
XFrame 点击劫持防护 (XFrame Handler)
通过下发 X-Frame-Options 标头,精确控制当前站点页面是否允许被第三方站点嵌入 <iframe>、<frame> 或 <object> 中,从而彻底防范钓鱼点击劫持攻击:
// 彻底禁止在任何 iframe 中加载当前页面
router.route().handler(XFrameHandler.create(XFrameHandler.DENY));
// 或者仅允许同源站点加载
router.route().handler(XFrameHandler.create(XFrameHandler.SAMEORIGIN));
OAuth2 授权码流程与各平台集成 (OAuth2AuthHandler Handler)
OAuth2AuthHandler 极大地简化了基于 OAuth2 标准协议(特别是 AuthCode 授权码重定向模式)保护 Web 资源的复杂度。开箱支持包括 GitHub、Google、Keycloak、Azure AD、Facebook、Twitter 等众多主流 IdP 服务商:
OAuth2Auth authProvider = GithubAuth
.create(vertx, "CLIENT_ID", "CLIENT_SECRET");
OAuth2AuthHandler oauth2 = OAuth2AuthHandler
.create(vertx, authProvider, "https://myserver.com/callback");
// 绑定接收 OAuth2 回调的路由端点
oauth2.setupCallback(router.route("/callback"));
// 拦截保护 /protected/* 路径下的所有资源
router.route("/protected/*").handler(oauth2);
router.route("/protected/somepage")
.handler(ctx -> ctx.response().end("欢迎进入受保护的核心资源专区!"));
WebAuthn4J 免密与生物凭证 (WebAuthn4J)
传统密码极易遭受社工猜解或撞库窃取。WebAuthn(Web 身份认证)是由 W3C 与 FIDO 联盟联合制定的现代化开放标准,允许用户借助硬件安全密钥(如 YubiKey)或手机/电脑内置的生物特征识别(指纹 TouchID、面容 FaceID)实现密码学公钥免密登录。
Vert.x 基于 WebAuthn4J 库提供了原生集成,支持安全挑战码下发、公钥凭证注册绑定与快速认证验签。
OTP 双因子认证 (One Time Password MFA)
Vert.x 完整支持多因素认证(MFA):
-
HOTP:基于哈希计数器的一次性密码 -
TOTP:基于时间窗口的一次性动态口令(如 Google Authenticator、Microsoft Authenticator)
通过 OtpAuthHandler 能够快速搭建包含二维码扫码绑定注册、动态 6 位口令校验及会话升级的安全鉴权防线。
HTTP 方法覆写
由于某些严格企业内网防火墙或陈旧客户端仅支持 GET 和 POST,RESTful API 中的 PUT、DELETE 无法直接发出。业界通常在 POST 请求中携带 X-HTTP-METHOD-OVERRIDE: PUT 请求头来表明真实意图。
Vert.x-Web 提供了 MethodOverrideHandler,内置安全的防篡改降级策略(Safe-Downgrade Policy),自动将请求透明重写为目标 HTTP 动词,无缝打通 REST 规范。
健康检查集成 (Health Check Handler)
通过挂载 Vert.x Health Checks 模块提供的健康检查 Handler,可以快速将业务探针、数据库连通性检查与 Kubernetes 的 Liveness / Readiness 探针深度集成,实现微服务自愈与自动化运维。