如何构建带会话认证的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()生成新的密钥,会导致之前的会话全部失效。
修复步骤
配置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(), ))持久化会话密钥
不要每次启动服务都生成新密钥,建议从环境变量或配置文件读取固定密钥,避免重启后会话失效:// 从环境变量读取密钥,长度至少32字节 let secret_key = Key::from( std::env::var("SESSION_SECRET") .expect("SESSION_SECRET环境变量必须设置") .as_bytes() );你可以生成一个随机密钥并设置到环境变量中,比如用OpenSSL生成:
openssl rand -hex 32
内容的提问来源于stack exchange,提问作者piccoloser
相关产品推荐
相关产品推荐

