登录返回前同步认证投影

新增 create_auth_session_and_sync,统一密码、手机号和微信登录会话的投影同步时机。

避免登录接口签发的新 access token 在下一次请求被当前或其它节点的 SpacetimeDB 校验拒绝。

补充登录投影同步回归测试。
This commit is contained in:
2026-09-02 14:55:52 +08:00
parent e9591c0543
commit 1c6c499a11
6 changed files with 98 additions and 27 deletions
@@ -21,12 +21,26 @@ pub struct SignedAuthSession {
pub refresh_token: String,
}
pub fn create_password_auth_session(
/// 创建登录会话并立即同步认证投影。
///
/// `create_auth_session` 只更新 api-server 进程内的认证工作集。登录响应返回前必须
/// 完成一次投影同步,否则客户端拿到的 access token 会在下一次请求经过
/// `validate_auth_session` 时被其它节点或当前节点的 SpacetimeDB 校验拒绝。
pub async fn create_auth_session_and_sync(
state: &AppState,
user: &AuthUser,
session_client: &SessionClientContext,
session_provider: AuthLoginMethod,
) -> Result<SignedAuthSession, AppError> {
create_auth_session(state, user, session_client, AuthLoginMethod::Password)
let signed_session = create_auth_session(state, user, session_client, session_provider)?;
state
.sync_auth_store_tables_to_spacetime()
.await
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
.with_message(format!("同步认证状态失败:{error}"))
})?;
Ok(signed_session)
}
#[cfg(not(test))]
@@ -193,3 +207,48 @@ fn map_access_token_device_info(client_info: &RefreshSessionClientInfo) -> Acces
client_platform: client_info.client_platform.clone(),
}
}
#[cfg(test)]
mod tests {
use axum::http::HeaderMap;
use super::*;
use crate::{config::AppConfig, session_client::resolve_session_client_context};
#[tokio::test]
async fn create_auth_session_and_sync_persists_the_new_session_projection() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let user = state
.seed_test_phone_user_with_password("13800138099", "secret123")
.await;
let session_client = resolve_session_client_context(&HeaderMap::new());
assert!(
!state.test_auth_projection_is_synced(),
"the seeded user is a local change until the login path syncs it"
);
let signed_session =
create_auth_session_and_sync(&state, &user, &session_client, AuthLoginMethod::Password)
.await
.expect("auth session should be created and synchronized");
assert!(state.test_auth_projection_is_synced());
let claims = platform_auth::verify_access_token(
&signed_session.access_token,
state.auth_jwt_config(),
)
.expect("access token should be valid");
assert_eq!(claims.user_id(), user.id);
assert!(
state
.refresh_session_service()
.is_session_active_for_user(
claims.user_id(),
claims.session_id(),
time::OffsetDateTime::now_utc(),
)
.expect("session state should be readable")
);
}
}
@@ -13,7 +13,7 @@ use crate::{
auth_payload::map_auth_user_payload,
auth_session::{
attach_set_cookie_header, build_refresh_session_cookie_header,
create_password_auth_session, record_daily_login_tracking_event_after_auth_success,
create_auth_session_and_sync, record_daily_login_tracking_event_after_auth_success,
},
http_error::AppError,
request_context::RequestContext,
@@ -80,7 +80,13 @@ pub async fn password_entry(
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
.with_message("账号服务暂不可用,请稍后重试"));
}
let signed_session = create_password_auth_session(&state, &result.user, &session_client)?;
let signed_session = create_auth_session_and_sync(
&state,
&result.user,
&session_client,
AuthLoginMethod::Password,
)
.await?;
record_daily_login_tracking_event_after_auth_success(
&state,
&request_context,
@@ -17,7 +17,7 @@ use crate::{
auth_payload::map_auth_user_payload,
auth_session::{
attach_set_cookie_header, build_clear_refresh_session_cookie_header,
build_refresh_session_cookie_header, create_auth_session,
build_refresh_session_cookie_header, create_auth_session_and_sync,
record_daily_login_tracking_event_after_auth_success,
},
http_error::AppError,
@@ -139,19 +139,13 @@ pub async fn reset_password(
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
.with_message("账号服务暂不可用,请稍后重试"));
}
let signed_session = create_auth_session(
let signed_session = create_auth_session_and_sync(
&state,
&result.user,
&session_client,
module_auth::AuthLoginMethod::Password,
)?;
state
.sync_auth_store_tables_to_spacetime()
.await
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
.with_message(format!("同步认证状态失败:{error}"))
})?;
)
.await?;
record_daily_login_tracking_event_after_auth_success(
&state,
&request_context,
@@ -19,8 +19,8 @@ use crate::{
api_response::json_success_body,
auth_payload::map_auth_user_payload,
auth_session::{
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
record_daily_login_tracking_event_after_auth_success,
attach_set_cookie_header, build_refresh_session_cookie_header,
create_auth_session_and_sync, record_daily_login_tracking_event_after_auth_success,
},
http_error::AppError,
platform_errors::{attach_retry_after, map_phone_auth_platform_store_error},
@@ -256,12 +256,13 @@ pub async fn phone_login(
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
.with_message("账号服务暂不可用,请稍后重试"));
}
let signed_session = create_auth_session(
let signed_session = create_auth_session_and_sync(
&state,
&result.user,
&session_client,
AuthLoginMethod::Phone,
)?;
)
.await?;
record_daily_login_tracking_event_after_auth_success(
&state,
&request_context,
+9 -1
View File
@@ -1309,7 +1309,11 @@ impl AppState {
pub async fn sync_auth_store_tables_to_spacetime(&self) -> Result<(), SpacetimeClientError> {
#[cfg(test)]
return Ok(());
{
self.auth_projection_synced_revision
.store(self.auth_store.revision(), Ordering::Release);
return Ok(());
}
#[cfg(not(test))]
let _sync_guard = self.auth_projection_sync_lock.lock().await;
@@ -1769,6 +1773,10 @@ fn creation_entry_feature_gates_require_user_tags(
#[cfg(test)]
impl AppState {
pub(crate) fn test_auth_projection_is_synced(&self) -> bool {
self.auth_projection_synced_revision.load(Ordering::Acquire) == self.auth_store.revision()
}
pub(crate) fn seed_test_refresh_session_for_user(
&self,
user: &module_auth::AuthUser,
+11 -8
View File
@@ -24,8 +24,8 @@ use crate::{
auth::AuthenticatedAccessToken,
auth_payload::map_auth_user_payload,
auth_session::{
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
record_daily_login_tracking_event_after_auth_success,
attach_set_cookie_header, build_refresh_session_cookie_header,
create_auth_session_and_sync, record_daily_login_tracking_event_after_auth_success,
},
http_error::AppError,
platform_errors::{attach_retry_after, map_wechat_provider_error},
@@ -318,12 +318,13 @@ pub async fn handle_wechat_callback(
))
.into_response());
}
let signed_session = create_auth_session(
let signed_session = create_auth_session_and_sync(
&state,
&result.user,
&session_client,
session_provider.clone(),
)?;
)
.await?;
record_daily_login_tracking_event_after_auth_success(
&state,
&request_context,
@@ -451,12 +452,13 @@ pub async fn bind_wechat_phone(
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
.with_message("账号服务暂不可用,请稍后重试"));
}
let signed_session = create_auth_session(
let signed_session = create_auth_session_and_sync(
&state,
&result.user,
&session_client,
AuthLoginMethod::Wechat,
)?;
)
.await?;
if result.activated_new_user {
crate::registration_reward::grant_new_user_registration_wallet_reward(
&state,
@@ -550,12 +552,13 @@ pub async fn login_wechat_mini_program(
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
.with_message("账号服务暂不可用,请稍后重试"));
}
let signed_session = create_auth_session(
let signed_session = create_auth_session_and_sync(
&state,
&result.user,
&session_client,
AuthLoginMethod::Wechat,
)?;
)
.await?;
let mut response_headers = HeaderMap::new();
attach_set_cookie_header(