rust web框架rocket指南——请求
请求
总之,为了路由的处理器能被调用,路由属性和函数签名指定路由必须是正确的。 你已经看到了这样一个例子:
#[get("/world")]
fn handler() { .. }
这个路由指定了它仅匹配到/world
的GET
请求。Rocket在调用处理器之前会验证这一点。当然,你可以做的不仅仅是指定请求的路径和方法。除了其它事情,Rocket还可以自动地进行数据验证:
- 路径动态参数的类型。
- 路由动态参数的个数。
- 请求体的数据类型。
- 查询参数、表单和表单值的类型。
- 请求预期发出和接收的格式。
- 任何用户自己定义的安全验证规则。
路由属性和函数签名共同描述了这些验证规则。Rocket的代码生成器实际担任了验证这些数据的工作。这一节主要讲怎样使用Rocket来进行这些数据验证和其他验证。
方法(Methods)
Rocket路由属性可以是 get
、put
、post
、delete
、patch
和 options
中的任意一个,或者任意一个和HTTP方法能匹配上的属性。例如,下面的属性匹配的是 根路径的 POST方法的请求:
#[post("/")]
属性的语法都的在正式的定义在rocket_codegen
API文档中 。
HEAD请求
当存在一个GET路由时,Rocket会自动处理对应路由的HEAD
请求。如果能够匹配的上,Rocket将原来的响应体过滤掉,作为HEAD
路由的响应。你也可以为HEAD
请求单独声明一个路由; Rocket并不会干涉你程序中对HEAD
请求的处理。
重解析
因为浏览器只能发送GET
和POST
请求,Rocket在特定条件下会重解析请求的方法。如果POST
请求的header
里包含Content-Type:application/x-www-form-urlencoded
,并且表单的第一个字段名为_method
,值为HTTP请求的合法方法(例如"PUT"
),Rocket将会以这个值中的方法,作为这次请求的方法。这会使Rocket应用程序可以提交非POST
的表单。例子todo 里面用了这个特性,通过网页表单提交PUT
和DELETE
请求。
动态路径参数
将变量名用尖括号括起来,放在路径中,可设置动态的路径参数。例如,如果我们向任何事说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
。提供的全部类型列表,请看FromParam
API 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
,&str
,Cow<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_int
和user_str
都设置了rank
参数。如果我们把这几个路由挂载到根路径下,运行程序之后,向/user/<id>
的请求,会按照一下的规则去匹配:
-
user
函数的路由会最先匹配。如果在<id>
位置的字符串是一个无符号的整形数字,那么user
函数就会被调用。如果不是,则请求就会被转向下一个路由:user_int
。 -
user_int
的路由会是第二个进行匹配。如果<id>
是有符号的整形,则user_int
函数被调用,反之请求会被转发到下一个路由。 -
user_str
的路由最后一个进行匹配。因为<id>
肯定是一个字符串,所以到达这个路由的请求都会被匹配。函数user_str
就会被调用。
路径动态参数可以为Result
或Option
类型。例如,如果在user
函数中参数id
为Result<usize, &RawStr>
,那么所有的请求都会被user
处理,不再转发到下一个。Ok
状态则表示<id>
是一个有效的usize
,然而Err
状态则表示<id>
并不是有效的usize
。Err
的值则会转换不成usize
的那个原始字符串。
值得注意的是,如果将user_str
和user_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
,B
和C
。 处理器函数的参数,不是路径路径参数的情况下才会被认为是请求警卫。因此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
中认证管理员用户。因此,任何含有AdminUser
或Apikey
参数的处理器,只有在请求符合预期条件的时候才会被调用。
请求保护将规则集中起来,使程序更简单,更稳定,更安全。
警卫规则
请求警卫和匹配规则是强大的校验组合。为了说明,我们考虑一个简单的鉴权功能是怎么实现的。
我们以两个请求警卫开始:
-
User
:普通的授权用户。
User
的FromRequest
实现会检测含有用户认证信息的cookie
,如果用户可以被认证则返回一个有效的User
,如果认证失败,则转向下一个路由。 -
AdminUser
: 管理员用户。
AdminUser
的FromRequest
实现会检测含有管理员认证信息的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_private
,add_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_key
。openssl
生成一个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) { .. }