Я работаю над внутренним 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."))
}
Я настроил свой клиент для работы с хостом 0.0.0.0 на порту 80, но с моими небольшими знаниями в области сетей это лучшее, что я мог придумать — я потерялся здесь. Любая помощь будет принята с благодарностью.





Как оказалось, cookie не передавался, потому что наша локальная сеть не использует https.
Изменение этого...
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone(),
))
к следующему...
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
.cookie_secure(false)
.build(),
)
решает проблему.