rust web框架rocket指南——请求

请求

总之,为了路由的处理器能被调用,路由属性和函数签名指定路由必须是正确的。 你已经看到了这样一个例子:

#[get("/world")]
fn handler() { .. }

这个路由指定了它仅匹配到/worldGET请求。Rocket在调用处理器之前会验证这一点。当然,你可以做的不仅仅是指定请求的路径和方法。除了其它事情,Rocket还可以自动地进行数据验证:

  • 路径动态参数的类型。
  • 路由动态参数的个数。
  • 请求体的数据类型。
  • 查询参数、表单和表单值的类型。
  • 请求预期发出和接收的格式。
  • 任何用户自己定义的安全验证规则。

路由属性和函数签名共同描述了这些验证规则。Rocket的代码生成器实际担任了验证这些数据的工作。这一节主要讲怎样使用Rocket来进行这些数据验证和其他验证。

方法(Methods)

Rocket路由属性可以是 getputpostdeletepatchoptions中的任意一个,或者任意一个和HTTP方法能匹配上的属性。例如,下面的属性匹配的是 根路径的 POST方法的请求:

#[post("/")]

属性的语法都的在正式的定义在rocket_codegen API文档中

HEAD请求

当存在一个GET路由时,Rocket会自动处理对应路由的HEAD请求。如果能够匹配的上,Rocket将原来的响应体过滤掉,作为HEAD路由的响应。你也可以为HEAD请求单独声明一个路由; Rocket并不会干涉你程序中对HEAD请求的处理。

重解析

因为浏览器只能发送GETPOST请求,Rocket在特定条件下会重解析请求的方法。如果POST请求的header里包含Content-Type:application/x-www-form-urlencoded,并且表单的第一个字段名为_method,值为HTTP请求的合法方法(例如"PUT"),Rocket将会以这个值中的方法,作为这次请求的方法。这会使Rocket应用程序可以提交非POST的表单。例子todo 里面用了这个特性,通过网页表单提交PUTDELETE请求。

动态路径参数

将变量名用尖括号括起来,放在路径中,可设置动态的路径参数。例如,如果我们向任何事说hello!,不仅仅是world,我们可以这样来定义路由:

#[get("/hello/<name>")]
fn hello(name: &RawStr) -> String {
    format!("Hello, {}!", name.as_str())
}

如果我们把这个路由挂载到根路径(.mount("/", routes![hello]),任何一个以hello开头且不为空的两部分的路由都会分发的这个hello路由。例如,如果我们访问/hello/John,程序会响应Hello, John!

动态路径参数允许任意数量。路径参数也可以是任意类型,包括自定义类型,但是自定义类型需要实现FromParam特性。Rocket已经对标准库中的许多类型和几个特殊的Rocket类型实现了FromParam。提供的全部类型列表,请看FromParamAPI docs。下面的完整路由可以说明各种用法:

#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: String, age: u8, cool: bool) -> String {
    if cool {
        format!("You're a cool {} year old, {}!", age, name)
    } else {
        format!("{}, we need to talk about your coolness.", name)
    }
}

原始字符串

你可能在上面例子的代码里注意到了一个不熟悉的类型 RawStr 。这是Rocket提供的特殊类型,表示直接从HTTP信息中获取的不明确的,没有验证的,没有解码的,原始字符串。String&strCow<str>表示的验证过的字符串,他们的区别是,&RawStr用来获取未经验证的输入。它提供了的方法很方便的可以将未验证的字符串转化为验证过的字符串。

&RawStr实现了FromParam特性,因此在上面的例子中,它可以作为路径动态参数的类型。当作为路径动态参数的类型时,RawStr指向一个潜在的未解码的字符串。 相比之下,String可以保证是解码之后的。使用哪一个,取决于你的目的,如果允许不安全的访问则使用&RawStr,反之则使用String

匹配规则

让我们认真看一看上面最后一个例子的属性和签名:

#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: String, age: u8, cool: bool) -> String { ... }

如果cool不是一个布尔类型呢?如果age不是u8类型呢?如果出现参数类型匹配不上的情况,Rocket会将请求转向下一个匹配的路由(如果存在的话)。Rocket会一直匹配直到完全匹配或者所有的路由都不能匹配。如果所有的路由都匹配不上,就会返回一个可自定义的404 error。

路由会根据一个升序规则做尝试匹配。Rocket的默认排序为-4到-1,详细规则会在下一节讲,所有路由的排序都可以通过rank属性手动设定。请看下面的例子:

#[get("/user/<id>")]
fn user(id: usize) -> T { ... }

#[get("/user/<id>", rank = 2)]
fn user_int(id: isize) -> T { ... }

#[get("/user/<id>", rank = 3)]
fn user_str(id: &RawStr) -> T { ... }

可以看到在函数user_intuser_str都设置了rank参数。如果我们把这几个路由挂载到根路径下,运行程序之后,向/user/<id>的请求,会按照一下的规则去匹配:

  1. user函数的路由会最先匹配。如果在<id>位置的字符串是一个无符号的整形数字,那么user函数就会被调用。如果不是,则请求就会被转向下一个路由:user_int
  2. user_int的路由会是第二个进行匹配。如果<id>是有符号的整形,则user_int函数被调用,反之请求会被转发到下一个路由。
  3. user_str的路由最后一个进行匹配。因为<id>肯定是一个字符串,所以到达这个路由的请求都会被匹配。函数user_str就会被调用。

路径动态参数可以为ResultOption类型。例如,如果在user函数中参数idResult<usize, &RawStr>,那么所有的请求都会被user处理,不再转发到下一个。Ok状态则表示<id>是一个有效的usize,然而Err状态则表示<id>并不是有效的usizeErr的值则会转换不成usize的那个原始字符串。

值得注意的是,如果将user_struser_int路由中的rank参数去掉,Rocket会在启动程序的时候发出一个error,表示路由冲突,或者是路由匹配了相似的请求。rank参数就是解决这个冲突的。

默认排序

如果没有显示的指定排序,Rocket会默认的分配排序。默认情况下,静态路由和含有查询参数的路由排序较小(优先匹配),动态路径参数路由和没有查询参数的路由排序较大(滞后匹配)。下面的表格显示了各种路由的默认排序。

static path query string rank example
yes yes -4 /hello?world=true
yes no -3 /hello
no yes -2 /<hi>?world=true
no no -1 /<hi>

多段参数

在路由中使用<param..>可以使用多段路径参数。这些参数的类型被叫做多段参数(segments),必须实现FromSegments。多段参数必须放在路径的最后面:如果在多段参数后面还有任何文本,则会产生编译错误。

下面的例子中,路由会匹配所有以/page/开头的请求:

#[get("/page/<path..>")]
fn get_page(path: PathBuf) -> T { ... }

/page/后面的路径都会有效的传入path参数。PathBuf实现了FromSegments防止了多段参数受到路径遍历攻击。因此,一个安全、稳定的静态稳定建服务器只用四行代码就可以实现:

#[get("/<file..>")]
fn files(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("static/").join(file)).ok()
}

格式

路由可以通过format参数指定接受的request请求或者返回的response的数据格式。参数的值为指定HTTP媒体类型的一个字符串。例如,JSON数据参数值为application/json

当路由指定的方法为带有请求体的方法(PUT, POST, DELETE, 和 PATCH),指定format属性之后,Rockt就会检测新来的请求的header中的Content-Type。只有请求的Content-Type和参数format中的值一直的时候才能匹配该路由。

请思考下面的例子:

#[post("/user", format = "application/json", data = "<user>")]
fn new_user(user: Json<User>) -> T { ... }

post属性中format参数声明了,新来的请求中,仅仅为Content-Type: application/json才能匹配new_user路由。(参数data会在下一节中讲到)。

当路由指定的是没有请求体的方法(GET, HEAD, 和 OPTIONS),指定format参数之后,Rocket会检测新来的请求中header中的Accept。仅仅在header的Accept中指定的希望收到的媒体类型和format参数指定的一致的请求才会匹配。

请思考下面的例子:

#[get("/user/<id>", format = "application/json")]
fn user(id: usize) -> Json<User> { ... }

get属性中的format参数指明了,在新来的请求中header中的Accept指定的媒体类型为application/json时才能匹配user

请求警卫

请求警卫是Rocket最强大的工具之一。按它名字的意思,请求警卫的作用是,根据请求包含的数据防止处理器被错误的调用。更确切的说,请求警卫是一个表示任意验证策略的类型。验策略则通过实现FromRequest特性来时先。任意一种实现了FromRequest的类型都是一个请求警卫。

请求警卫在向处理器中传参的时候起作用。作为路由处理器的参数,请求警卫可以设置任意数量。在调用处理器之前,Rocket会调用请求警卫对FromRequest 的实现。只有当请求通过所有的警卫的时候,Rocket才会调用处理器处理请求。

来看下面的例子,下面虚拟的处理器函数用了三个请求警卫:A,BC。 处理器函数的参数,不是路径路径参数的情况下才会被认为是请求警卫。因此param并不是请求警卫。

#[get("/<param>")]
fn index(param: isize, a: A, b: B, c: C) -> ... { ... }

请求警卫的执行顺序是从左到右。上面的例子中执行顺序是A B C。失败是短路的;如果一个警卫失败,剩下的就不会执行。了解更多关于请求警卫的信息以及实现请求警卫,请看FromRequest 文档。

自定义警卫

你可以为你自己的类型实现FromRequest。如下面的例子,你可以创建一个ApiKey类型,并为其实现FromRequest, 然后将其作为请求警卫。只有在请求头中存在ApiKey的时候,路由sensitive才会运行。

#[get("/sensitive")]
fn sensitive(key: ApiKey) -> &'static str { ... }

你也可以为AdminUser类型实现FromRequest,用来从cookies中认证管理员用户。因此,任何含有AdminUserApikey参数的处理器,只有在请求符合预期条件的时候才会被调用。
请求保护将规则集中起来,使程序更简单,更稳定,更安全。

警卫规则

请求警卫和匹配规则是强大的校验组合。为了说明,我们考虑一个简单的鉴权功能是怎么实现的。
我们以两个请求警卫开始:

  • User:普通的授权用户。
    UserFromRequest实现会检测含有用户认证信息的cookie,如果用户可以被认证则返回一个有效的User,如果认证失败,则转向下一个路由。

  • AdminUser: 管理员用户。
    AdminUserFromRequest实现会检测含有管理员认证信息的cookie,如果用户可以被认证则返回一个有效的AdminUser,如果认证失败,则转向下一个路由。

现在我们将两个警卫和请求匹配规则组合起来实现三个路由,每个路由都指向/admin的认证控制处面板。

#[get("/admin")]
fn admin_panel(admin: AdminUser) -> &'static str {
    "Hello, administrator. This is the admin panel!"
}

#[get("/admin", rank = 2)]
fn admin_panel_user(user: User) -> &'static str {
    "Sorry, you must be an administrator to access this page."
}

#[get("/admin", rank = 3)]
fn admin_panel_redirect() -> Redirect {
    Redirect::to("/login")
}

上述三条路由定制了认证和授权。 admin_panel的路由仅在管理员登录时才会执行。之后才会显示管理面板。 如果用户不是管理员,路由则会匹配下一个。接下来会尝试顺序为第二的admin_panel_user路由。 如果任意用户为登陆状态,则会执行此路由,并显示“对不起,你没有管理员权限”。 最后,如果用户未登录,则尝试admin_panel_redirect路由。 由于这个路由没有警卫,所以总会成功执行。用户重新返回到登录页面。

Cookies

Cookies 是一个重要的内建的请求警卫:你可以获取,设置,和删除cookies。因为Cookies是一个请求警卫,因此Cookies的类型可以作为处理器的参数:

use rocket::http::Cookies;

#[get("/")]
fn index(cookies: Cookies) -> Option<String> {
    cookies.get("message")
        .map(|value| format!("Message: {}", value))
}

因此cookise可以在处理器中使用。上面的例子中,获取了cookies中的message信息。Cookies警卫也可以设置或者删除cookies信息。GitHub上的cookies例子说明了更多是用Cookies类型操作cookies的方法,同时Cookies 文档包含了所有的使用方法。

加密Cookies

通过Cookies::add() 方法添加cookies是“显而易见的”,所有的值都能被客户端看到。对于敏感数据,Pocket提供了加密cookies。

加密cookies和常规的cookies类似,只是经过了认证模式加密,认证模式加密同时提供了机密性,完成行,和真实性。这意味着加密cookies不能被客户检查,篡改或制造。 如果您愿意,可以将加密cookies视为签名和加密。

加密cookies的获取,添加,和删除的API和常规的相同,只是方法末尾多了_private。分别是:get_privateadd_private,和 remove_private。使用的例子如下:

/// Retrieve the user's ID, if any.
#[get("/user_id")]
fn user_id(cookies: Cookies) -> Option<String> {
    cookies.get_private("user_id")
        .map(|cookie| format!("User ID: {}", cookie.value()))
}

/// Remove the `user_id` cookie.
#[post("/logout")]
fn logout(mut cookies: Cookies) -> Flash<Redirect> {
    cookies.remove_private(Cookie::named("user_id"));
    Flash::success(Redirect::to("/"), "Successfully logged out.")
}

密匙

Rocket使用256bit的密匙加密cookies,密匙在配置参数secret_key中指定。如果不指定,Rocket会自动生成一个新密匙。需要注意的是,加密cookie的解密密匙必须和加密密匙相同才能解密。因此,如果当程序重启之后还要正确解密之前加密的cookie,就必须在配置中指定secret_key。如果在正式环境中程序启动时发现配置中没有指定secret_key,Rocket会发出一个警告。

通常使用openssl之类的工具来生成合适的secret_keyopenssl生成一个256bit的base64密匙使用命令openssl rand -base64 32

关于配置的更多信息,请看本指南的配置(Configuration) 这一节。

一次一个

为了安全起见,目前Rocket要求在同一时间最多只能有一个活跃的Cookies实例。多个Cookies实例的情况并不常见,但是一旦遇到,处理器就会不知所措。

如果真的出现,Roocket会在console里输出如下信息:

=> Error: Multiple `Cookies` instances are active at once.
=> An instance of `Cookies` must be dropped before another can be retrieved.
=> Warning: The retrieved `Cookies` instance will be empty.

当违反这个规则调用处理器时,就会输出上述日志。解决这个问题只能是调用处理器的时候,保证统一时间只能有一个Cookies。大家共同容易犯的一个错误是,同时使用Cookies警卫和Custom警卫,并且通过Custom警卫又获取了一次Cookies。如下:

#[get("/")]
fn bad(cookies: Cookies, custom: Custom) { .. }

因为首先验证Cookies警卫,之后在Custom警卫里再次获取Cookies实例的时候,已经存在一个Cookies了。
这个方案可以简单的通过调换警卫的顺序实现:

#[get("/")]
fn good(custom: Custom, cookies: Cookies) { .. }

请求体

和Rocket大部分时候一样,请求体数据处理是明确数据类型的。 标明handler希望的数据类型,需要声明data="<param>", 这里的param就是handler的一个参数。
这个参数的类型必须实现了FromData trait。看起来类似下面的例子,T就假定实现了FromData:

#[post("/", data = "<input>")]
fn new(input: T) { /* .. */ }

任何实现了 FromData 的类型,同时也实现了 类型限制

表单

在web应用程序中,表单是处理的最多的类型。Rocket 处理表单 很容易。假设应用程序需要处理一个todo Task表单, 表单 包含两个字段:完成状态,一个复选框和一个描述、一个文字字段。像下面的例子,在Rocket里你可以很容易的处理表单请求:

use rocket::request::Form;

#[derive(FromForm)]
struct Task {
    complete: bool,
    description: String,
}

#[post("/todo", data = "<task>")]
fn new(task: Form<Task>) { /* .. */ }

松散的解析

Rocket 的FromForm 默认是严格的解析。Form<T> 只有在form精确的包含T的所有字段才会解析成功。如果form字段缺少或者多于Form<T>就会解析失败。比如,提交的form 含有 "a", "b", "c" 三个字段 但是 T只含有 "a"和"c"两个字段,from就没办法解析成Form<T>

Rocket 可以选择不使用严格模式解析。使用LenientForm<T>类型实现。 当提交的form 含有T的字段的超集的时候, LenientForm<T> 就会自动过滤掉多余的字段,解析成功。比如,当提交的 form含有“a”,"b","c"三个字段,并且T只含有“a”,"c"两个字段,这个form能够成功解析成LenientForm<T>

You can use a LenientForm anywhere you'd use a Form. Its generic parameter is also required to implement FromForm. For instance, we can simply replace Form with LenientForm above to get lenient parsing:
在任何使用Form的地方都可以使用LenientForm。他的泛型参数一样需要实现FromForm。 例如,我们吧上面的例子中的Form简单的替换成LenientForm就可以使用松散的解析了。

use rocket::request::LenientForm;

#[derive(FromForm)]
struct Task {
    /* .. */
}

#[post("/todo", data = "<task>")]
fn new(task: LenientForm<Task>) { /* .. */ }

Rocket系列 >>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容