Vert.x-Web的讲解和使用(二)

时间:2021-12-23 18:03:44



接上章《Vert.x-Web的讲解和使用(一)》

错误处理程序

你不但可以设置处理程序去处理请求,同样你也可以设置一个错误处理程序在你的Router中。

错误处理程序被使用在精确的有精确的匹配规则的普通Router上。

例如:你可以提供一个错误处理程序在你认为有可能发生错误的请求路径或者Http方法上。

这一点允许你在应用程序的不同部分设置不同的错误处理程序。

这里有一个例子:当路径为/somepath/的GET请求开始出现错误的时候,错误处理程序将会响应。

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

route.failureHandler(frc -> {

// This will be called for failures that occur
// when routing requests to paths starting with
// '/somepath/'

});

如果一个处理程序抛出一个异常或者抛出一个特殊的HTTP的状态码的话,这个错误处理程序就会触发。

如果一个应用程序抛出了一个异常,程序会抛出一个500的HTTP状态码。

处理错误时,错误处理程序可以通过异常处理程序的上下文得到异常或者异常状态码然后用来恢复相应。

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

route1.handler(routingContext -> {

// Let's say this throws a RuntimeException
throw new RuntimeException("something happened!");

});

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

route2.handler(routingContext -> {

// This one deliberately fails the request passing in the status code
// E.g. 403 - Forbidden
routingContext.fail(403);

});

// Define a failure handler
// This will get called for any failures in the above handlers
Route route3 = router.get("/somepath/*");

route3.failureHandler(failureRoutingContext -> {

int statusCode = failureRoutingContext.statusCode();

// Status code will be 500 for the RuntimeException or 403 for the other failure
HttpServerResponse response = failureRoutingContext.response();
response.setStatusCode(statusCode).end("Sorry! Not today");

});


请求主体处理

BodyHandler允许你检索请求的主体,限制主体的大小和处理文件上传。

你因该确保有一个主体处理程序在你的匹配所有的请求的Router上。

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

获得请求主体

如果你知道请求的内容是个Json的字符串的话,你就可以使用 getBodyAsJson()方法获取,如果是一个字符串,你可以用getBodyAsString()获取,如果是一个缓冲区的话,可以用getBody().


限制请求主体大小

在创建BodyHandler的时候可以使用setBodyLimit()方法去限制请求主体数据的字节的大小。这样做对大的请求耗尽系统内存是非常有用的。

如果试图去发送一个大于设置数值的请求主体时,一个状态码是413-请求主体过大的响应将被送回客户端。

请求主体没有默认的大小(即可以无限大)。

合并表格属性

默认情况下,BodyHandler会合并请求的所有表格属性。如果你不想这个行为发生,你可以使用setMergeFormAttributes()方法禁用它。


处理文件上传

BodyHandler还可以处理多文件上传。

如果一个BodyHandler在匹配请求路径,任何文件上传都会被保存到默认的上产路径下。

每一个被上传的文件都会被自动分配一个名字,上传的文件可以从上下文中通过fileUploads()方法获得。

如下例子:

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

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

Set<FileUpload> uploads = routingContext.fileUploads();
// Do something with uploads....

});

每一个上传的文件都被描述为一个FileUpload实例,你可以访问它的名字,文件名称,和大小。

CookieHandler

Vert.x-Web使用CookieHandler支持Cookie。

如果你想使用这个功能的话你需要在你的匹配所有请求的Router上指定一个CookieHandler.

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

操作Cookie

你可以使用getCookie方法通过名称获得Cookie,或者通过cookies获得名字的集合。

想要删除一个Cookie的话可以使用removeCookie。

添加一个Cookie使用addCookie.

浏览器可以存储被自动写入响应头的Cookie设置。

这里有一个查询和创建Cookie的例子:

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

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

Cookie someCookie = routingContext.getCookie("mycookie");
String cookieValue = someCookie.getValue();

// Do something with cookie...

// Add a cookie - this will get written back in the response automatically
routingContext.addCookie(Cookie.cookie("othercookie", "somevalue"));
});

会话处理

Vert.x-Web提供开箱即用的Session支持。

Session可以用于一个浏览器发起的多个持续性的请求中的数据存储和传递,就像网络中的购物车一样。

Vert.x-Web使用一个会话Cookie来定义一个Session.这个会话Cookie是临时的并且在你的浏览器关闭的时候就会被删除。

你不能放任何实际的数据到这个会话Cookie中,因为这个Cookie中仅仅存储了一个用于找到服务器中保存的实际Session的唯一标识。

这个唯一标志是自动生成的一个UUID,所以它是不固定。

Cookie 在HTTP请求和响应之间传递。所以如果Session被使用的时候,Vert.x总是自作聪明的确定你正在使用HTTPS。

如果你企图使用Session去结束一个连续的HTTP时,Vert.x会警告你的。

如果你想要在你的程序中启用Session你需要在你的程序匹配Route之前设置上一个SessionHandler。

SessionHandler处理Session的创建和管理。


会话存储

想要创建一个SessionHandler你需要拥有一个SessionStore的实例,SessionStore是一个对象用来在你的程序中存储实际的Session。

Vert.x-Web附带两个SessionStore的实现。如果你喜欢的你同样可以实现自己的SessionStore。


本地会话存储

LocalSessionStore是本地存储在内存的。

如果在你的应用程序有一个使用了会话粘连的单一的Vert.x的实例或者你配置了负载均衡所有的HTTP请求路由到同一个Vert.x实例上。那么LocalSessionStore是非常适用的。

如果你不能保证你的请求都终止在同一台服务器上或者你不知道你的请求会终止在那一台服务器上就不要使用这个SessionStore。

LocalSessionStore基于共享本地Map实现的同时它还实现了一个收割者用于清除过期的会话。

这个收割者的工作间隔在LocalSessionStore创建时可以被配置。

这里有一个创建LocalSessionStore的例子:

 SessionStore store1 = LocalSessionStore.create(vertx);

// Create a local session store specifying the local shared map name to use
// This might be useful if you have more than one application in the same
// Vert.x instance and want to use different maps for different applications
SessionStore store2 = LocalSessionStore.create(vertx, "myapp3.sessionmap");

// Create a local session store specifying the local shared map name to use and
// setting the reaper interval for expired sessions to 10 seconds
SessionStore store3 = LocalSessionStore.create(vertx, "myapp3.sessionmap", 10000);


集群会话存储

ClusteredSessionStore存储整个集群都能访问的到的分布式Map中。

如果你不使用粘性会话,即你的负载均衡分发同一个浏览器的不同请求到不同的服务器上。那么你可以使用这个SessionStore。

你的Session可以通过集群中的任何一个节点使用这个SessionStore。

你在使用ClusteredSessionStore的时候你需要确定你的Vert.x实例是集群的。

这里有一个创建ClusteredSessionStore的例子:

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

Vertx vertx = res.result();

// Create a clustered session store using defaults
SessionStore store1 = ClusteredSessionStore.create(vertx);

// Create a clustered session store specifying the distributed map name to use
// This might be useful if you have more than one application in the cluster
// and want to use different maps for different applications
SessionStore store2 = ClusteredSessionStore.create(vertx, "myclusteredapp3.sessionmap");
});

创建SessionHandler

一旦你创建完一个SessionStore后,你就可以创建一个SessionHandler并且把它添加到一个Route上。你需要确保你的SessionHandler在你的应用程序处理程序之前。

在你创建一个SessionHandler时应包含一个创建完成的CookieHandler,因为SessionHandler需要使用一个Cookie查找Session。

这里有一个例子:

 Router router = Router.router(vertx);

// We need a cookie handler first
router.route().handler(CookieHandler.create());

// Create a clustered session store using defaults
SessionStore store = ClusteredSessionStore.create(vertx);

SessionHandler sessionHandler = SessionHandler.create(store);

// Make sure all requests are routed through the session handler too
router.route().handler(sessionHandler);

// Now your application handlers
router.route("/somepath/blah/").handler(routingContext -> {

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

});

SessionHandler会确保你的Session在到达你的应用处理程序前从SessionStore查找(如果不存在就创建)并且被设置到请求的上下文中。

使用Session

在你的处理程序中你可以通过session获得Session的实例。

你可以使用put方法将数据放进Session,get方法获取数据,remove方法删除数据。

在LocalSessionStore中Session数据项的Key值通常是一个String字符串,Value可以是任何类型的数据,在ClusteredSessionStore中通常是一些基本数据类型,或者Buffer,JsonObject,JsonArray

或者一些序列化的类型,集群需要可序列化的数据用于传递。

这里有一个多数据Session的例子:

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

// Now your application handlers
router.route("/somepath/blah").handler(routingContext -> {

Session session = routingContext.session();

// Put some data from the session
session.put("foo", "bar");

// Retrieve some data from a session
int age = session.get("age");

// Remove some data from a session
JsonObject obj = session.remove("myobj");

});

Session通常在响应完成后将数据写回SessionStore。

你可以使用destroy方法手动的销毁一个Session。这样会在请求上下文和SessionStore中删除一个Session。

注意:如果浏览器的请求再次被SessionHandler获取的话,被删除的Session将会被重建。


Session超时

如果你两个请求之间的时间大于Session设定的超时时间后,Session将会超时,超时的Session将会被从SessionStore清除。

当请求到达的时候,Session将会被找到并且被自动标记,当响应完成后,Session会被存储回SeesionStore。

你可以手动的调用setAccessed方法去标记一个Session为以访问。

Session的超时时间是可以在创建SessionHandler的时候被指定。如果不指定默认的超时时间是30分钟。


身份验证/授权

Vert.x提供了一些开箱即用的处理程序来处理身份验证和授权。

创建身份验证处理程序

在创建身份验证处理程序之前,你需要有一个AuthProvider实例。AuthProvider被使用在身份的验证和用户授权。

在vertx-auth项目中Vert.x提供了几个开箱即用的AuthProvider实例。如何完整的使用和配置AuthProvider请参考身份验证文档。

这里提供了使用AuthProvider创建基本的AuthHandler的例子:

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

// All requests to paths starting with '/private/' will be protected
router.route("/private/*").handler(basicAuthHandler);

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

// This will be public access - no login required

});

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

// This will require a login

// This will have the value true
boolean isAuthenticated = routingContext.user() != null;

});

如果AuthHandler成功的通过了用户的身份验证和授权,它将会注入一个User进入RoutingContext,所以你可以从你的处理程序中使用user方法获得它。

入股你想使你的User对象保存到Session中去,以便于子以后的请求中不用再去验证。那么你需要在AuthHandler之前指定一个SessionHandler和一个UserSessionHandler。

一旦你获得了你的User对象你可以编码去给它授权。

如果你想导致用户注销,你可在RoutingContext中调用clearUser方法。


HTTP基本身份验证

HTTP Basic Authentication 是一个简单的适合简单应用程序的身份验证。

在这里,未加密的证书被封装到HTTP头中,你的程序中必须使用HTTPS来请求。

如果一个用户请求一个需要认证的资源时,它会返回一个401的响应。这提示浏览器显示一个登录框和提示用户输入用户名和密码。

再次请求这个资源时,如果这次在Authorization中包含用Base64编码的用户名和密码。

当基本真正处理程序收到这些信息时,它调用AuthProvider配置用户名和密码进行身份验证的用户。如果验证成功处理程序将会试图授权给用户。

如果授权成功,用户请求的路径将会被转到指定的处理程序执行,另外的情况就是返回403的响应状态码意味着访问被拒绝。

这个权限处理程序可以被设置一组授权对于需要被访问的资源的授予。


重定向身份验证处理程序

重定向身份验证处理程序被用于当用户请求一个受到保护的程序的时候,如果用户没有登录,使用户的请求跳转到一个登录页面。

当用户填充并且提交登录表单后,服务器的处理程序认证用户,如果认证完成,然后再次重定向回到用户请求的原来的资源。

要想使用重定向验证,你需要在跟Route上配置一个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);

// All requests to paths starting with '/private/' will be protected
router.route("/private/*").handler(redirectAuthHandler);

// Handle the actual login
router.route("/login").handler(FormLoginHandler.create(authProvider));

// Set a static server to serve static resources, e.g. the login page
router.route().handler(StaticHandler.create());

router.route("/someotherpath").handler(routingContext -> {
// This will be public access - no login required
});

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

// This will require a login

// This will have the value true
boolean isAuthenticated = routingContext.user() != null;

});


JWT授权

JWT授权为资源提供保护权限,以便于拒绝没有权限的资源访问。

使用它需要两个步骤:

1.为处理程序提供一个可用的令牌(或者依赖第三方)

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 -> {
// this is an example, authentication should be done with another provider...
if ("paulo".equals(ctx.request().getParam("username")) && "secret".equals(ctx.request().getParam("password"))) {
ctx.response().end(authProvider.generateToken(new JsonObject().put("sub", "paulo"), new JWTOptions()));
} else {
ctx.fail(401);
}
});

现在你的客户端有了一个令牌,它会填充到你随后的每一个HTTP请求的Header中的Authorization参数中。

 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 -> {
// some handle code...
});

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

配置请求权限(Configuring required authorities)

在任何授权处理程序中,你都可以配置请求权限去响应资源。

默认情况下,如果没有配置权限,用户登录后就可以去访问资源,否则,用户就必须登录(权限分配)和拥有访问权限。

这里有一个例子,程序的不同模块被配置了不同的权限。注意:这意味着权限视潜在的权限提供者而定,例如 有些人使用role/permission基本模型,

但是其他人可能使用另一个模型。

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

// Need "list_products" authority to list products
router.route("/listproducts/*").handler(listProductsAuthHandler);

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

// Only "admin" has access to /private/settings
router.route("/private/settings/*").handler(settingsAuthHandler);

静态资源服务(Serving static resources

Vert.x为静态web资源提供了一个开箱即用的处理程序,这个样你就可以很容易的写静态的WEB服务器了。

你需要一个StaticHandler的实例用于服务像 .html .css .js 或其他的静态资源。

任何被StaticHandler处理的请求都会被返回一个系统目录或者classpath下的文件。静态资源的默认目录是webroot,我们可以手动配置它为其他路径。

下面的例子所有以/static/开始的请求将会被webroot目录服务:

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

对于这个例子,如果请求的路径是:/static/css/mystyle.css,静态服务将会寻找webroot/static/css/mystyle.css.

它同样会寻找在classpath下寻找这个文件。这意味着你可以将你的静态文件打包进一个jar文件(或者fatjar)并且分发它。

当Vert.x在classpath下找到一个资源的时候,它会提取这个文件并将它缓存到硬盘上的一个临时目录中。只有第一次才会这样做,并不是每次都这样。

配置高速缓存(Configuring caching

默认情况下,静态处理程序设置Cache Headers以便有效的使用浏览器缓存。

Vert.x-Web可以设置Header的 cache-control,last-modified,和date.

cache-control默认情况下被设置为 max-age=86400,刚好是一天。你可以通过setMaxAgeSeconds改变它。

如果一个浏览器发送来一个get请求但是请求的HEAD的if-modified-since表示资源在这一天后没有修改过,会返回一个304的状态用于告诉浏览器去使用自己的本地缓存。

如果不需要处理Cache Headers,你可以使用setCachingEnabled去禁用它。

当启用缓存处理,将在内存中缓存资源的最后修改时间。这可以避免每次都去检查资源的最终修改日期。

如果你知道你的文件在磁盘上永远都不会更改,那么这条缓存将永远都不会过期。这是默认的。

如果你知道你的文件在服务器运行期间有可能改变。那么你就可以使用setFilesReadOnly设置这个文件只读为false。

在任何一个时间点你都可以调用setMaxCacheSize来设置缓存中可存在的数据条数的大小。

你可以使用setCacheEntryTimeout设置一条缓存的过期时间。

配置主页( Configuring the index page

任何请求到根目录/将会被主页响应,默认的主页是 index.html。你可以调用setIndexPage方法来设定主页。

修改webroot(Changing the web root

默认的静态资源的目录是webroot,配置它请调用setWebRoot方法。

隐藏文件配置(Serving hidden files

默认情况下,服务器是使用隐藏文件的。

如果你不想隐藏文件被使用,你可以用setIncludeHIdden方法配置它。

目录清单(Directory listing

服务器还可以提供目录清单。默认情况下目录清单是不启用的。你可以使用setDirectoryListing启用它。

启用目录清单时返回的内容取决于响应的Header的Content-type.

为 text/html目录清单可以使用setDirectoryTemplate方法指定模板来渲染目录清单页面。