Skip to content
Clone
8923bc0
198 lines 7.5 KB Rust
use crate::models::users::{user_sessions, users};
use crate::session::Session;
use chrono::{Duration, Utc};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set};
use uuid::Uuid;
use crate::share::{crypto, user as store};
use crate::{AuthError, AuthResult, AuthService};
/// 会话凭据摘要的算法标识,落在 `user_sessions.hash_key_id`。
pub const SESSION_HASH_KEY_ID: &str = "logical-session-v1";
/// 会话 ID 的摘要。会话 ID 本身即凭据,库里只存摘要。
pub fn session_token_hash(id: Uuid) -> String {
crypto::sha256_hex(id.to_string().as_bytes())
}
impl AuthService {
#[tracing::instrument(skip_all, fields(user_id = user.id, mfa = mfa, request_id = %request_id.unwrap_or_default()))]
pub async fn bind_auth_session(
&self,
session: &Session,
user: &users::Model,
mfa: bool,
request_id: Option<&str>,
) -> AuthResult<()> {
let now = Utc::now();
let id = Uuid::now_v7();
let ttl = i64::try_from(self.session_max_age_secs())
.unwrap_or(86400)
.max(1);
user_sessions::ActiveModel {
id: Set(id),
user_id: Set(user.id),
token_hash: Set(session_token_hash(id)),
hash_key_id: Set(SESSION_HASH_KEY_ID.into()),
security_version: Set(user.security_version),
expires_at: Set(now + Duration::seconds(ttl)),
mfa_verified_at: Set(mfa.then_some(now)),
ip_address: Set(session.client_ip()?),
user_agent: Set(session.user_agent()?),
..Default::default()
}
.insert(self.db().write())
.await?;
session.set("auth_session_id", id.to_string())?;
session.set("mfa_verified", mfa)?;
session.set_user_id(user.id)?;
session.set_security_version(user.security_version)?;
session.renew();
Ok(())
}
#[tracing::instrument(skip_all, fields(user_id = user_id, session_id = %session_id, request_id = %request_id.unwrap_or_default()))]
pub async fn check_session(
&self,
user_id: i64,
version: i32,
session_id: &str,
request_id: Option<&str>,
) -> AuthResult<(users::Model, bool)> {
// security_version 判定一律读主库:副本延迟会让刚吊销的旧版本短暂放行。
let user = users::Entity::find_by_id(user_id)
.one(self.db().write())
.await?
.ok_or(AuthError::Unauthenticated)?;
if !{
let user = &user;
user.id > 0
&& user.deleted_at.is_none()
&& user.allow_use
&& (!user.forbid_login
|| user
.endtime_forbid_login_at
.is_none_or(|until| until > Utc::now()))
} {
return Err(AuthError::Unauthenticated);
}
if version <= 0 || version != user.security_version {
return Err(AuthError::Unauthenticated);
}
let id = Uuid::parse_str(session_id).map_err(|_| AuthError::Unauthenticated)?;
let row = user_sessions::Entity::find_by_id(id)
.one(self.db().write())
.await?
.ok_or(AuthError::Unauthenticated)?;
// `token_hash` 是会话凭据的摘要:只有拿着明文会话 ID 的一方能算出来。
// 不校验它,单有数据库读权限的人就能直接拿行里的 ID 复会话。
if !crate::share::crypto::constant_time_eq(
row.token_hash.as_bytes(),
session_token_hash(id).as_bytes(),
) {
return Err(AuthError::Unauthenticated);
}
if row.user_id != user_id
|| row.security_version != version
|| row.revoked_at.is_some()
|| row.expires_at <= Utc::now()
{
return Err(AuthError::Unauthenticated);
}
Ok((user, row.mfa_verified_at.is_some()))
}
#[tracing::instrument(skip_all, fields(request_id = %request_id.unwrap_or_default()))]
pub async fn require_session(
&self,
session: &Session,
request_id: Option<&str>,
) -> AuthResult<i64> {
let user_id = session.user_id()?.ok_or(AuthError::Unauthenticated)?;
let version = session
.security_version()?
.ok_or(AuthError::Unauthenticated)?;
let id = session
.get::<String>("auth_session_id")?
.ok_or(AuthError::Unauthenticated)?;
let _ = self
.check_session(user_id, version, &id, request_id)
.await?;
Ok(user_id)
}
pub async fn user(&self, user_id: i64) -> AuthResult<users::Model> {
users::Entity::find_by_id(user_id)
.one(self.db().read())
.await?
.ok_or(AuthError::UserNotFound)
}
pub async fn users(&self, user_ids: &[i64]) -> AuthResult<Vec<users::Model>> {
let mut unique: Vec<i64> = Vec::with_capacity(user_ids.len());
for user_id in user_ids.iter().copied().filter(|user_id| *user_id > 0) {
if !unique.contains(&user_id) {
unique.push(user_id);
}
}
if unique.is_empty() {
return Ok(Vec::new());
}
Ok(users::Entity::find()
.filter(users::Column::Id.is_in(unique))
.all(self.db().read())
.await?)
}
pub async fn update(&self, user_id: i64, patch: serde_json::Value) -> AuthResult<users::Model> {
let user = store::load_active_user(self.db(), user_id).await?;
let version = patch
.get("version")
.and_then(|value| value.as_i64())
.ok_or(AuthError::MalformedRequest)?;
if i64::from(user.version) != version {
return Err(AuthError::VersionMismatch {
current: user.version,
});
}
let mut active = user.clone().into_active_model();
if let Some(name) = patch.get("username").and_then(|value| value.as_str()) {
let name = crate::share::validate::validate_username(name)
.map_err(|_| AuthError::InvalidUsername)?;
active.lowername = Set(name.to_lowercase());
active.username = Set(name);
}
if let Some(value) = patch.get("displayname") {
active.displayname = Set(match value.as_str() {
Some(value) => Some(
crate::share::validate::validate_display_name(value)
.map_err(|_| AuthError::InvalidDisplayname)?,
),
None if value.is_null() => None,
_ => return Err(AuthError::MalformedRequest),
});
}
if let Some(value) = patch.get("public_show_email") {
active.public_show_email = Set(value.as_bool().ok_or(AuthError::MalformedRequest)?);
}
active.version = Set(user
.version
.checked_add(1)
.ok_or(AuthError::VersionMismatch {
current: user.version,
})?);
active.updated_at = Set(Utc::now());
let updated = users::Entity::update_many()
.set(active)
.filter(users::Column::Id.eq(user_id))
.filter(users::Column::Version.eq(user.version))
.exec_with_returning(self.db().write())
.await?
.pop()
.ok_or(AuthError::VersionMismatch {
current: user.version,
})?;
crate::services::me::invalidate_me_cache(self.cache(), user_id).await;
Ok(updated)
}
}