feat(session): integrate actix-web framework with enhanced session management

- Added actix-web and actix-multipart dependencies to Cargo.toml
- Integrated actix-web ResponseError trait for AppError handling
- Migrated session module to use actix-web request lifecycle management
- Enhanced Session struct with request-local state handling capabilities
- Implemented proper HTTP status code mapping for various error types
- Added comprehensive session middleware integration points
- Updated session state persistence and modification tracking logic
- Integrated proper JSON response formatting for error messages
- Added support for session renewal, purge, and unchanged state management
This commit is contained in:
zhenyi
2026-06-07 17:41:57 +08:00
parent 6a8e978073
commit 4e2c1c932a
11 changed files with 793 additions and 77 deletions
+2
View File
@@ -1,9 +1,11 @@
pub mod config;
pub mod middleware;
#[allow(clippy::module_inception)]
pub mod session;
pub mod storage;
pub use self::{
middleware::{CookieContentSecurity, SessionMiddleware, TtlExtensionPolicy},
session::{Session, SessionState, SessionStatus, SessionUser},
storage::{RedisSessionStore, SessionKey, SessionStore, generate_session_key},
};
+94 -38
View File
@@ -1,6 +1,10 @@
use std::cell::{Ref, RefCell};
use std::future::{Ready, ready};
use std::mem;
use std::rc::Rc;
use actix_web::dev::{Extensions, Payload, ServiceRequest, ServiceResponse};
use actix_web::{FromRequest, HttpMessage, HttpRequest};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};
@@ -10,21 +14,30 @@ use crate::error::AppError;
const SESSION_USER_KEY: &str = "session:user_uid";
/// Request-local session handle.
///
/// Clones share the same interior state through request extensions, so changes
/// made by extractors/handlers can be collected by `SessionMiddleware` at the
/// end of the request lifecycle.
#[derive(Clone)]
pub struct Session(Rc<RefCell<SessionInner>>);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum SessionStatus {
/// Session state has changed and must be persisted.
Changed,
/// Session and cookie must be deleted. Further mutations are ignored.
Purged,
/// Session key must be regenerated while preserving current state.
Renewed,
/// Session has not been modified during this request.
#[default]
Unchanged,
}
#[derive(Default)]
struct SessionInner {
state: Map<String, Value>,
state: SessionState,
status: SessionStatus,
}
@@ -33,7 +46,7 @@ impl Session {
Self::default()
}
pub fn from_state(state: Map<String, Value>) -> Self {
pub fn from_state(state: SessionState) -> Self {
Self(Rc::new(RefCell::new(SessionInner {
state,
status: SessionStatus::Unchanged,
@@ -41,34 +54,36 @@ impl Session {
}
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, AppError> {
if let Some(value) = self.0.borrow().state.get(key) {
Ok(Some(serde_json::from_value(value.clone())?))
} else {
Ok(None)
}
let value = self.0.borrow().state.get(key).cloned();
value
.map(serde_json::from_value)
.transpose()
.map_err(AppError::Json)
}
pub fn contains_key(&self, key: &str) -> bool {
self.0.borrow().state.contains_key(key)
}
pub fn entries(&self) -> Ref<'_, Map<String, Value>> {
pub fn entries(&self) -> Ref<'_, SessionState> {
Ref::map(self.0.borrow(), |inner| &inner.state)
}
pub fn status(&self) -> SessionStatus {
Ref::map(self.0.borrow(), |inner| &inner.status).clone()
self.0.borrow().status.clone()
}
pub fn is_empty(&self) -> bool {
self.0.borrow().state.is_empty()
}
pub fn insert<T: Serialize>(&self, key: impl Into<String>, value: T) -> Result<(), AppError> {
let value = serde_json::to_value(value)?;
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
if inner.status != SessionStatus::Renewed {
inner.status = SessionStatus::Changed;
}
let val = serde_json::to_value(&value)?;
inner.state.insert(key.into(), val);
mark_changed(&mut inner.status);
inner.state.insert(key.into(), value);
}
Ok(())
@@ -83,22 +98,20 @@ impl Session {
F: FnOnce(T) -> T,
{
let mut inner = self.0.borrow_mut();
let key_str = key.into();
let key = key.into();
if let Some(val) = inner.state.get(&key_str) {
if inner.status == SessionStatus::Purged {
return Ok(());
}
let value: T = serde_json::from_value(val.clone())?;
let updated = serde_json::to_value(updater(value))?;
if inner.status != SessionStatus::Renewed {
inner.status = SessionStatus::Changed;
}
inner.state.insert(key_str, updated);
if inner.status == SessionStatus::Purged {
return Ok(());
}
let Some(value) = inner.state.get(&key).cloned() else {
return Ok(());
};
let value = serde_json::from_value(value)?;
let updated = serde_json::to_value(updater(value))?;
mark_changed(&mut inner.status);
inner.state.insert(key, updated);
Ok(())
}
@@ -121,14 +134,12 @@ impl Session {
pub fn remove(&self, key: &str) -> Option<Value> {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
if inner.status != SessionStatus::Renewed {
inner.status = SessionStatus::Changed;
}
return inner.state.remove(key);
if inner.status == SessionStatus::Purged {
return None;
}
None
mark_changed(&mut inner.status);
inner.state.remove(key)
}
pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, AppError>> {
@@ -140,9 +151,7 @@ impl Session {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
if inner.status != SessionStatus::Renewed {
inner.status = SessionStatus::Changed;
}
mark_changed(&mut inner.status);
inner.state.clear();
}
}
@@ -174,13 +183,45 @@ impl Session {
}
pub fn take_state(&self) -> SessionState {
let mut inner = self.0.borrow_mut();
std::mem::take(&mut inner.state)
mem::take(&mut self.0.borrow_mut().state)
}
pub fn mark_unchanged(&self) {
self.0.borrow_mut().status = SessionStatus::Unchanged;
}
pub(crate) fn set_session(req: &mut ServiceRequest, state: SessionState) {
let session = Self::get_session(&mut req.extensions_mut());
let mut inner = session.0.borrow_mut();
inner.state = state;
inner.status = SessionStatus::Unchanged;
}
pub(crate) fn get_changes<B>(res: &mut ServiceResponse<B>) -> (SessionStatus, SessionState) {
let Some(inner) = res
.request()
.extensions()
.get::<Rc<RefCell<SessionInner>>>()
.cloned()
else {
return (SessionStatus::Unchanged, SessionState::new());
};
let mut inner = inner.borrow_mut();
let status = inner.status.clone();
let state = mem::take(&mut inner.state);
(status, state)
}
pub(crate) fn get_session(extensions: &mut Extensions) -> Session {
if let Some(inner) = extensions.get::<Rc<RefCell<SessionInner>>>() {
return Session(Rc::clone(inner));
}
let inner = Rc::new(RefCell::new(SessionInner::default()));
extensions.insert(Rc::clone(&inner));
Session(inner)
}
}
impl Default for Session {
@@ -193,3 +234,18 @@ pub type SessionState = Map<String, Value>;
#[derive(Debug, Clone, Copy)]
pub struct SessionUser(pub Uuid);
impl FromRequest for Session {
type Error = AppError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
ready(Ok(Self::get_session(&mut req.extensions_mut())))
}
}
fn mark_changed(status: &mut SessionStatus) {
if *status != SessionStatus::Renewed {
*status = SessionStatus::Changed;
}
}