You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何构建带会话认证的actix-web HttpServer?局域网会话失效求助

局域网访问Actix Web会话丢失问题解决

我正在开发一个内部API,授权用户通过它读写数据库,程序计划在本地局域网运行,支持多用户/应用访问。目前本地用localhost访问功能正常,但用自身IP登录或从局域网其他电脑访问时,会话无法保存,无法进行任何操作。这是我第一次实现认证功能,经验不足可能导致错误。

Cargo.toml依赖

actix-session = { version = "0.7.1", features = ["cookie-session"] }
actix-web = "^4"
argon2 = "0.4.1"
rand_core = "0.6.3"
reqwest = "0.11.11"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.85"
sqlx = { version = "0.6.1", features = ["runtime-actix-rustls", "mysql", "macros"] }

main.rs代码

use actix_session::storage::CookieSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
use actix_web::web::{get, post, Data, Path};
use actix_web::{HttpResponse, Responder};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let secret_key = Key::generate();

    // Load or create a new config file.
    // let settings = ...

    // Create a connection to the database.
    let pool = sqlx::mysql::MySqlPoolOptions::new()
        .connect(&format!(
            "mysql://{}:{}@{}:{}/mydb",
            env!("DB_USER"),
            env!("DB_PASS"),
            env!("DB_HOST"),
            env!("DB_PORT"),
        ))
        .await
        .unwrap();

    println!(
        "Application listening on {}:{}",
        settings.host,
        settings.port,
    );

    // Instantiate the application and add routes for each handler.
    actix_web::HttpServer::new(move || {
        let logger = actix_web::middleware::Logger::default();
        actix_web::App::new()
            .wrap(SessionMiddleware::new(
                CookieSessionStore::default(),
                secret_key.clone(),
            ))
            .wrap(logger)
            .app_data(Data::new(pool.clone()))
            /*
                Routes that return all rows from a database table.
            */
            /*
                Routes that return a webpage.
            */
            .route("/new", get().to(new))
            .route("/login", get().to(login))
            .route("/register", get().to(register))
            /*
                Routes that deal with authentication.
            */
            .route("/register", post().to(register_user))
            .route("/login", post().to(login_user))
            .route("/logout", get().to(logout_user))
            /*
                Routes that handle POST requests.
            */
    })
    .bind(format!("{}:{}", settings.host, settings.port))?
    .run()
    .await
}

认证相关代码

use crate::model::User;
use actix_session::Session;
use actix_web::web::{Data, Form};
use actix_web::{error::ErrorUnauthorized, HttpResponse};
use argon2::password_hash::{rand_core::OsRng, PasswordHasher, SaltString};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use sqlx::{MySql, Pool};

#[derive(serde::Serialize)]
pub struct SessionDetails {
    user_id: u32,
}

#[derive(Debug, sqlx::FromRow)]
pub struct AuthorizedUser {
    pub id: u32,
    pub username: String,
    pub password_hash: String,
    pub approved: bool,
}

pub fn check_auth(session: &Session) -> Result<u32, actix_web::Error> {
    match session.get::<u32>("user_id").unwrap() {
        Some(user_id) => Ok(user_id),
        None => Err(ErrorUnauthorized("User not logged in.")),
    }
}

pub async fn register_user(
    data: Form<User>,
    pool: Data<Pool<MySql>>,
) -> Result<String, Box<dyn std::error::Error>> {
    let data = data.into_inner();
    let salt = SaltString::generate(&mut OsRng);

    let argon2 = Argon2::default();

    let password_hash = argon2
        .hash_password(data.password.as_bytes(), &salt)
        .unwrap()
        .to_string();

    // Use to verify.
    // let parsed_hash = PasswordHash::new(&hash).unwrap();

    const INSERT_QUERY: &str =
        "INSERT INTO users (username, password_hash) VALUES (?, ?) RETURNING id;";

    let fetch_one: Result<(u32,), sqlx::Error> = sqlx::query_as(INSERT_QUERY)
        .bind(data.username)
        .bind(password_hash)
        .fetch_one(&mut pool.acquire().await.unwrap())
        .await;

    match fetch_one {
        Ok((user_id,)) => Ok(user_id.to_string()),
        Err(err) => Err(Box::new(err)),
    }
}

pub async fn login_user(
    session: Session,
    data: Form<User>,
    pool: Data<Pool<MySql>>,
) -> Result<HttpResponse, Box<dyn std::error::Error>> {
    let data = data.into_inner();
    let fetched_user: AuthorizedUser = match sqlx::query_as(
        "SELECT id, username, password_hash, approved FROM users WHERE username = ?;",
    )
    .bind(data.username)
    .fetch_one(&mut pool.acquire().await?)
    .await
    {
        Ok(fetched_user) => fetched_user,
        Err(e) => return Ok(HttpResponse::NotFound().body(format!("{e:?}"))),
    };

    let parsed_hash = PasswordHash::new(&fetched_user.password_hash).unwrap();

    match Argon2::default().verify_password(&data.password.as_bytes(), &parsed_hash) {
        Ok(_) => {
            if !fetched_user.approved {
                return Ok(
                    HttpResponse::Unauthorized().body("This account has not yet been approved.")
                );
            }

            session.insert("user_id", &fetched_user.id)?;
            session.renew();

            Ok(HttpResponse::Ok().json(SessionDetails {
                user_id: fetched_user.id,
            }))
        }
        Err(_) => Ok(HttpResponse::Unauthorized().body("Incorrect password.")),
    }
}

pub async fn logout_user(session: Session) -> HttpResponse {
    if check_auth(&session).is_err() {
        return HttpResponse::NotFound().body("No user logged in.");
    }

    session.purge();
    HttpResponse::SeeOther()
        .append_header(("Location", "/login"))
        .body(format!("User logged out successfully."))
}

问题原因与解决方法

核心原因

Actix Session的CookieSessionStore默认将Cookie的域设置为localhost,当通过IP地址或局域网其他机器访问时,浏览器会认为当前请求的域与Cookie的域不匹配,不会携带会话Cookie,导致服务器无法识别用户会话。此外,每次启动服务时调用Key::generate()生成新的密钥,会导致之前的会话全部失效。

修复步骤

  1. 配置Cookie的域与属性
    修改SessionMiddleware的初始化代码,显式配置Cookie的域、路径、SameSite等属性,确保Cookie能在局域网环境下被正确携带:

    .wrap(SessionMiddleware::new(
        CookieSessionStore::builder()
            // 设置Cookie的域为你的服务器局域网IP,或泛域名(如网段为192.168.1.x,可设为".192.168.1")
            .cookie_domain(Some("192.168.1.100".to_string())) // 替换为你的服务器实际IP
            .cookie_path("/".to_string()) // 让Cookie对所有路由生效
            .cookie_same_site(actix_web::cookie::SameSite::Lax) // 允许跨域(局域网内)发送Cookie
            .cookie_secure(false) // 局域网无HTTPS时需设为false,否则浏览器不会发送Cookie
            .build(),
        secret_key.clone(),
    ))
    
  2. 持久化会话密钥
    不要每次启动服务都生成新密钥,建议从环境变量或配置文件读取固定密钥,避免重启后会话失效:

    // 从环境变量读取密钥,长度至少32字节
    let secret_key = Key::from(
        std::env::var("SESSION_SECRET")
            .expect("SESSION_SECRET环境变量必须设置")
            .as_bytes()
    );
    

    你可以生成一个随机密钥并设置到环境变量中,比如用OpenSSL生成:

    openssl rand -hex 32
    

内容的提问来源于stack exchange,提问作者piccoloser

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 10:25:21