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 实现 Session
由于 HTTP 是无状态的,所以我们可以通过cookie来维护状态。但 cookie 是直接保存到客户端,所以对于敏感数据,不能直接保存到 cookie。我们可以把敏感数据保存到服务端,然后把对应的 ID 保存到 cookie,这就是 Session。本章我们将使用 Cookie 和 Redis 实现一个简单的 Session。
本章将会通过一个简单的用户登录流程来演示 Session 的实现。
GET /
:用户信息首页。如果登录成功,从 Session 中读取已登录用户的信息;如果没有登录,提示用户进行登录
GET /login
:显示用户登录表单
POST /login
:处理用户登录。如果用户名和密码正确,将用户信息保存到 Session,并跳转到用户信息首页。
将用户信息保存到redis,并将Session ID写入cookie
代码如下:
读取 Session 的流程
从cookie中读取到Session ID,然后从 Redis 读取该ID对应的用户信息
/// 从 cookie 中获取session id
fn get_session_from_cookie(headers: &HeaderMap) -> Option<String> {
let cookies = headers
.get(axum::http::header::COOKIE)
.and_then(|value| value.to_str().ok())
.unwrap_or("");
if cookies.is_empty() {
return None;
}
let mut session_id: Option<String> = None;
let cookies: Vec<&str> = cookies.split(';').collect();
for cookie in cookies {
let cookie_pair: Vec<&str> = cookie.split('=').collect();
let cookie_name = cookie_pair[0].trim();
let cookie_value = cookie_pair[1].trim();
if cookie_name == SESSION_ID_COOKIE_NAME && !cookie_value.is_empty() {
session_id = Some(cookie_value.to_string());
break;
}
}
session_id
}
登录
首先,我们来看一下登录操作。获取用户提交的表单,并判断用户名和密码,如果验证通过,将用户信息序列化后保存到 Redis,并生成对应的 Session ID。将这个 Session ID 写入 Cookie,同时跳转到用户信息首页:
/// 登录操作
async fn logout_action(
Extension(rdc): Extension<redis::Client>,
Form(frm): Form<UserLoginForm>,
) -> Result<(StatusCode, HeaderMap, ()), String> {
let mut headers = HeaderMap::new();
let url;
if !(&frm.username == "axum.rs" && &frm.password == "axum.rs") {
url = "/login?msg=用户名或密码错误"
} else {
// 生成 session ID
let session_id = Uuid::new_v4().to_simple().to_string();
// 将 session ID 保存到 Cookie
save_session_id_to_cookie(&session_id, &mut headers);
let user_session = UserSession {
username: frm.username,
level: 1,
};
let user_session = serde_json::json!(user_session).to_string();
// 将 session 保存到 redis
let redis_key = format!("{}{}", SESSION_KEY_PREFIX, session_id);
let mut conn = rdc
.get_async_connection()
.await
.map_err(|err| err.to_string())?;
// session 将在20分钟后自动过期
conn.set_ex(redis_key, user_session, 1200)
.await
.map_err(|err| err.to_string())?;
url = "/"
}
headers.insert(axum::http::header::LOCATION, url.parse().unwrap());
Ok((StatusCode::FOUND, headers, ()))
}
用户信息首页
在用户信息首页,首先尝试从 Cookie 中获取 Session ID,获取到之后,通过这个 Session ID 从 Redis 读取出用户信息,并反序列化为结构体。如果 Cookie 中没有 Session ID 或者 Redis 中没有对应的用户信息,则提示需要登录。
/// 首页
async fn index(
Extension(rdc): Extension<redis::Client>,
headers: HeaderMap,
) -> Result<Html<String>, String> {
let session_id = get_session_from_cookie(&headers);
let mut session: Option<UserSession> = None;
if let Some(session_id) = session_id {
// 从 redis 读取 Session
let redis_key = format!("{}{}", SESSION_KEY_PREFIX, session_id);
let mut conn = rdc
.get_async_connection()
.await
.map_err(|err| err.to_string())?;
let session_str: Option<String> =
conn.get(redis_key).await.map_err(|err| err.to_string())?;
if let Some(session_str) = session_str {
let user_session: UserSession =
serde_json::from_str(&session_str).map_err(|err| err.to_string())?;
session = Some(user_session);
}
}
match session {
Some(session) => {
let html = format!(
r#"
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8" />
<meta name="author" content="axum.rs ([email protected])" />
<title>
用户首页-AXUM中文网
</title>
</head>
<body>
<div>欢迎 {} ! 你的等级是 {}。</div>
<div><a href="/logout">退出登录</a></div>
</body>
</html>"#,
session.username, session.level
);
Ok(Html(html))
}
None => Err("Please login via /login page".to_string()),
}
}
退出登录
首先从 Cookie 中获取到 Session ID,然后将对应的用户信息从 Redis 中删除。
/// 退出登录
async fn logout(
Extension(rdc): Extension<redis::Client>,
headers: HeaderMap,
) -> Result<(StatusCode, HeaderMap, ()), String> {
let session_id = get_session_from_cookie(&headers);
let mut headers = HeaderMap::new();
if let Some(session_id) = session_id {
// 从 redis 删除 Session
let redis_key = format!("{}{}", SESSION_KEY_PREFIX, session_id);
let mut conn = rdc
.get_async_connection()
.await
.map_err(|err| err.to_string())?;
conn.del(redis_key).await.map_err(|err| err.to_string())?;
// 清空Cookie
save_session_id_to_cookie(&session_id, &mut headers);
}
headers.insert(axum::http::header::LOCATION, "/login".parse().unwrap());
Ok((StatusCode::FOUND, headers, ()))
}
本章我们讨论了如何利用 Cookie 和 Redis 实现一个简单的 Session。涉及的代码有点多,请通过我们的代码仓库查看完整代码。