axum中的各种响应
本章主要讨论 axum 的响应。axum 已经实现了多种响应,比如纯文本、HTML、JSON 及 自定义响应头(response header)。除了这些 axum 内置的响应之外,我们还将讨论如何将自己定义的结构体,作为响应返回给客户端。在axum中获取请求数据
在日常开发中,我们需要与用户进行交互,从各种渠道获取用户输入,包括但不限于:表单、URL 参数、URL Path 以及 JSON 等。axum 为我们提供了这些获取用户输入的支持。axum的状态共享
**状态共享**是指,在整个应用或不同路由之间,共享一份数据。axum 提供了方便的状态共享机制,但可能也会踩坑。本章将带你学习如何在 axum web 应用中共享状态。路由
axum 提供了常用的 HTTP 请求方式对应的路由,比如 `get`, `post`, `put`, `delete` 等。除此之外,axum 还提供了“嵌套路由”。路由,通常和 `handler(处理函数)` 结合在一起。中间件
中间件是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。本章将讨论如何在axum中使用中间件,以及如何自定义中间件。axum处理静态文件
和其它 Web 框架一样,axum 也会对所有请求进行处理。对于 CSS、JS 及图片等静态文件,并不需要 axum 的 handler 进行处理,而是只需要简单的把它们的内容进行返回即可。axum 提供了处理静态文件的中间件。axum处理cookie
Cookie 是通过 HTTP Header 进行传递的。由某个响应头进行设置,然后其它请求头就可以获取到了。本章将通过模拟用户中心来用 axum 操作 HTTP Header 演示 Cookie 的读写操作。axum 操作 redis
通过 redis-rs 这个 crate,可以很方便的操作 redis。它提供了同步和异步两种连接,由于我们要集成到 axum 中,所以这里使用异步连接。本章将展示如何获取 redis 异步连接、如何将字符串保存到 redis、如何获取到保存在 redis 里的字符串以及如何通过 redis 保存和读取自定义结构体。axum 操作 Postgres 数据库
PostgreSQL 是一款天然支持异步操作的高性能开源关系型数据库。本章将讨论如何在 axum 中使用 PostgreSQL。包括:数据的增加、修改、删除、查找以及开始事务保证业务的原子性。axum 实现 Session
由于 HTTP 是无状态的,所以我们可以通过Cookie来维护状态。但 cookie 是直接保存到客户端,所以对于敏感数据,不能直接保存到 cookie。我们可以把敏感数据保存到服务端,然后把对应的 ID 保存到 cookie,这就是 Session。本章我们将使用 Cookie 和 Redis 实现一个简单的 Session。axum 集成 JWT
Json web token(JWT)是为了网络应用环境间传递声明而执行的一种基于 JSON 的开发标准(RFC 7519),该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。axum 中使用模板引擎
利用模板引擎,我们不需要再把 HTML 代码写在 Rust 代码中了,而是将其独立保存为`*.html`文件。既方便维护,也有利用开发。axum 上传文件
文件上传是 Web 开发中常见的功能,本章将演示如何在 axum 实现文件上传。axum 集成hcaptcha验证码进行人机验证
在机器人采集、恶意攻击的今天,人机验证筑起了一道保护网。从之前的图片验证码,到 Google 提供的 reCaptcha,人机验证经历了一段漫长的演进过程。配置文件:让axum app可配置
将数据库连接信息、redis 连接信息以及 Web 应用监听地址等信息通过配置文件进行单独管理是一个比较好的开发实践。这样就无须在更改配置的时候重新编译整个项目,同时也可以针对不同环境使用不同的配置文件。axum错误处理
本章主要讨论 axum 的错误处理
在axum中获取请求数据
在日常开发中,我们需要与用户进行交互,从各种渠道获取用户输入,包括但不限于:表单、URL 参数、URL Path 以及 JSON 等。axum 为我们提供了这些获取用户输入的支持。
获取 Path
参数
Path
参数,又称为“路径参数”,它既可以实现参数的传递,又对 SEO 友好。
什么是 Path
参数
假设有以下 URL:
https://github.com/axumrs/axum-rs
将其各部分进行分解,得到:
-
https
:传输协议 -
github.com
:主机名 -
axumrs/axum-rs
:路径。这部分就是Path
,我们可以将这部分变成参数化。
https
:传输协议
github.com
:主机名
axumrs/axum-rs
:路径。这部分就是 Path
,我们可以将这部分变成参数化。
参数化
我们可以把 axumrs/axum-rs
参数化为 用户名/仓库名
,这样就可以动态的显示不同用户的不同仓库了,基于此,我们把上面的 URL 变成:
https://github.com/:user/:repository
当然,对于相对固定的部分,我们可以不进行参数化,而是直接显式地进行声明,比如:
https://axum.rs/topic/roaming-axum/request
这个 URL 中,虽然 topic/roaming-axum/request
都是 Path 部分,但我们只需要对后面两个进行参数化,最终得到:
https://axum.rs/topic/:subject/:article
如何在 axum 获取 Path
参数
axum::extract
包中提供了众多 Extract
,其中的 Path
可以方便的获取 Path
参数。
// 定义handler
async fn user_info(Path(id): Path<i32>) -> String {
format!("User info for {}", id)
}
// 定义路由
route("/user/:id", get(user_info));
请注意此例中,handler 函数的参数:(Path(id): Path<i32>)
,其含义是:接收一个i32
类型的参数(Path<i32>
),并将其解构为id
变量(Path(id)
)。如果你没有解构,需要在代码中使用id.0
的形式获取到参数的值:
async fn user_info(id: Path<i32>) -> String {
format!("User info for {}", id.0)
}
如何获取多个 Path
参数
上文提到,如果没有对 Path
参数解构,可以通过 变量名.0
的方式获取到参数的值。从中我们可以将 Path
参数看作一个元组。
async fn repo_info(Path((user_name, repo_name)): Path<(String, String)>) -> String {
format!(
"Repository: user name: {} and repository name: {}",
user_name, repo_name
)
}
对应的路由定义:
route("/repo/:user/:repo", get(repo_info));
使用 curl 访问结果如下:
$ curl http://127.0.0.1:9527/repo/axumrs/axum-rs
Repository: user name: axumrs and repository name: axum-rs
将多个Path
参数填充为结构体
对于少量的参数,我们可以使用元组形式进行获取,但出于以下原因,我们更推荐将参数填充到结构体中:
-
结构体字段易于扩展
-
结构体更具明确性
结构体字段易于扩展
结构体更具明确性
要填充为结构体,我们需要进行以下几步:
定义结构体
pub struct RepoInfo {
pub user_name: String,
pub repo_name: String,
}
实现 trait
#[derive(Deserialize)]
pub struct RepoInfo {
将参数填充到结构体
async fn repo_info_struct(Path(info): Path<RepoInfo>) -> String {
format!(
"Repository: user name: {} and repository name: {}",
info.user_name, info.repo_name
)
}
路由定义
route("/repo_struct/:user_name/:repo_name", get(repo_info_struct));
访问结果示例:
$ curl http://127.0.0.1:9527/repo_struct/axumrs/axum-rs
Repository: user name: axumrs and repository name: axum-rs
注意:
- 路由定义中的参数名必须和结构体的字段名保持一致
- 结构体及其字段必须是可访问的
注意:
- 路由定义中的参数名必须和结构体的字段名保持一致
- 结构体及其字段必须是可访问的
获取 Url 参数
除了 Path
参数,我们还可以获取 Url 参数。Url 参数是指附加在网址后面,以?
开头的部分。它是一个键值对,多个参数间以&
分割。
https://axum.rs/subject?page=1&keyword=axum.rs
此例包含两个 Url 参数:
-
值为
1
的page
参数 -
值为
axum.rs
的keyword
参数
值为 1
的 page
参数
值为 axum.rs
的 keyword
参数
如何在 axum 获取 Url 参数
使用 Query
这个 Extract
可以获取到 Url 参数。
定义结构体并实现 trait
#[derive(Deserialize)]
pub struct SubjectArgs {
pub page: i32,
pub keyword: String,
}
将参数填充到结构体
async fn subject(Query(args): Query<SubjectArgs>) -> String {
format!("Page {}, keyword: {} of subjects", args.page, args.keyword)
}
访问结果示例:
$ curl 'http://127.0.0.1:9527/subject?page=1&keyword=axum.rs'
Page 1, keyword: axum.rs of subjects
上面的例子要求 page
和 keyword
必须传入,否则会报错。然而,在现实生活中,这两个参数往往是可选的。
试着将 handler 函数的参数进行修改,加上 Option
:
async fn subject_opt(args: Option<Query<SubjectArgs>>) -> String {
if let Some(args) = args {
let args = args.0;
return format!("Page {}, keyword: {} of subjects", args.page, args.keyword);
}
"Page 0, no keyword of subjects".to_string()
}
试着访问,会发现依然有问题:只有两个参数同时提供或两个参数同时不提供的时候,结果才是正确的。不符合我们的预期。我们的预期是,参数可以同时提供、同时不提供、也可以只提供其中某一个。
继续修改。
#[derive(Deserialize)]
pub struct SubjectArgsOpt {
pub page: Option<i32>,
pub keyword: Option<String>,
}
async fn subject_opt_done(Query(args): Query<SubjectArgsOpt>) -> String {
let page = args.page.unwrap_or(0);
let keyword = args.keyword.unwrap_or("".to_string());
format!("Page {}, keyword: {} of subjects", page, keyword)
}
我们将结构体的字段定义为 Option
,现在符合我们的要求。
默认的,axum 把 Url 参数映射成了HashMap
,所以我们可以将所有参数获取到一个 HashMap 中。
async fn all_query(Query(args): Query<HashMap<String, String>>) -> String {
format!("{:?}", args)
}
获取表单输入
类似的,axum 也提供了一个名 Form
的 Extract
用于获取表单输入。
定义结构体并实现 trait
#[derive(Deserialize)]
pub struct CreateUser {
pub username: String,
pub email: String,
pub level: u8,
}
获取表单输入
async fn create_user(Form(frm): Form<CreateUser>) -> String {
format!(
"Created user: {}, email: {}, level: {}",
frm.username, frm.email, frm.level
)
}
路由定义
route("/create_user", post(create_user));
注意,这里我们定义的是 POST 路由
访问结果示例:
$ curl http://127.0.0.1:9527/create_user -X POST -d 'username=axum.rs&[email protected]&level=1'
Created user: axum.rs, email: [email protected], level: 1
获取用户提交的 JSON 数据
将上例的 Form
改成 Json
就可以获取用户以 JSON 格式提交的数据了:
async fn create_user_ajax(Json(frm): Json<CreateUser>) -> String {
format!(
"Created user: {}, email: {}, level: {}",
frm.username, frm.email, frm.level
)
}
访问结果示例:
$ curl http://127.0.0.1:9527/create_user_ajax -H 'content-type:application/json' -X POST -d '{"username":"axum.rs","email":"[email protected]","level":1}'
Created user: axum.rs, email: [email protected], level: 1
获取所有请求头
axum 将请求头封装成了 HeaderMap
,通过 handler 参数可以很方便的获取到它。
async fn get_all_headers(headers: HeaderMap) -> String {
format!("{:?}", headers)
}
获取请求头数据
下面以 USER AGENT
为例,演示获取某一个请求头数据方法。
async fn get_user_agent(headers: HeaderMap) -> String {
headers
.get(axum::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(|v| v.to_string())
.unwrap()
}
获取已命名请求头数据
axum 提供了一种更加简便的方法来获取某个请求头的数据。称为 TypedHeader 。
这种方式在编写代码的时候非常简单:
async fn get_user_agent_typed(TypedHeader(user_agent): TypedHeader<UserAgent>) -> String {
user_agent.to_string()
}
省去了手动获取、转换 header 数据的步骤。但 axum 默认并没有启用该功能,需要在 Cargo.toml
中手动启用 headers
这个 feature,同时,需要加入名为 headers
的 crate:
axum = { version = "0.3", features = ["headers"]}
headers = "0.3"
本章详解了 axum 获取请求数据的几种方式,从中可以看出 axum 的可扩展性和灵活性。本章代码你可以在代码仓库中查看。
思考题:
-
如何读取 Cookie 数据?
-
如何写入 Cookie 数据?
如何读取 Cookie 数据?
如何写入 Cookie 数据?
提示:
- 读取和写入 Cookie 都是通过 Headers 完成
- 先在响应中写入,再从后续的请求中读取
提示:
- 读取和写入 Cookie 都是通过 Headers 完成
- 先在响应中写入,再从后续的请求中读取
思考完成之后,你可以看一下我们准备的《axum 处理 cookie》章节。