axum 实现 Session

由于 HTTP 是无状态的,所以我们可以通过cookie来维护状态。但 cookie 是直接保存到客户端,所以对于敏感数据,不能直接保存到 cookie。我们可以把敏感数据保存到服务端,然后把对应的 ID 保存到 cookie,这就是 Session。本章我们将使用 Cookie 和 Redis 实现一个简单的 Session。

本章将会通过一个简单的用户登录流程来演示 Session 的实现。

  • GET /:用户信息首页。如果登录成功,从 Session 中读取已登录用户的信息;如果没有登录,提示用户进行登录

  • GET /login:显示用户登录表单

  • POST /login:处理用户登录。如果用户名和密码正确,将用户信息保存到 Session,并跳转到用户信息首页。

  • GET /logout:退出登录。清空 Session

保存 Session 的流程

将用户信息保存到redis,并将Session ID写入cookie

代码如下:

读取 Session 的流程

从cookie中读取到Session ID,然后从 Redis 读取该ID对应的用户信息

代码如下:

登录

用户信息首页

在用户信息首页,首先尝试从 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。涉及的代码有点多,请通过我们的代码仓库查看完整代码。

要查看完整内容,请先登录