feat: 新增管理后台前端项目及安全加固
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor(saas): 重构认证中间件与限流策略
- 登录限流调整为5次/分钟/IP
- 注册限流调整为3次/小时/IP
- GET请求不计入限流
fix(saas): 修复调度器时间戳处理
- 使用NOW()替代文本时间戳
- 兼容TEXT和TIMESTAMPTZ列类型
feat(saas): 实现环境变量插值
- 支持${ENV_VAR}语法解析
- 数据库密码支持环境变量注入
chore: 新增前端管理界面
- 基于React+Ant Design Pro
- 包含路由守卫/错误边界
- 对接58个API端点
docs: 更新安全加固文档
- 新增密钥管理规范
- 记录P0安全项审计结果
- 补充TLS终止说明
test: 完善配置解析单元测试
- 新增环境变量插值测试用例
This commit is contained in:
@@ -19,14 +19,16 @@ const ACCESS_TOKEN_COOKIE: &str = "zclaw_access_token";
|
||||
const REFRESH_TOKEN_COOKIE: &str = "zclaw_refresh_token";
|
||||
|
||||
/// 构建 auth cookies 并附加到 CookieJar
|
||||
/// secure 标记在开发环境 (ZCLAW_SAAS_DEV=true) 设为 false,生产设为 true
|
||||
fn set_auth_cookies(jar: CookieJar, token: &str, refresh_token: &str) -> CookieJar {
|
||||
let access_max_age = std::time::Duration::from_secs(2 * 3600); // 2h
|
||||
let refresh_max_age = std::time::Duration::from_secs(7 * 86400); // 7d
|
||||
let secure = !is_dev_mode();
|
||||
|
||||
// cookie crate 需要 time::Duration,从 std 转换
|
||||
let access = Cookie::build((ACCESS_TOKEN_COOKIE, token.to_string()))
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.secure(secure)
|
||||
.same_site(SameSite::Strict)
|
||||
.path("/api")
|
||||
.max_age(access_max_age.try_into().unwrap_or_else(|_| std::time::Duration::from_secs(3600).try_into().unwrap()))
|
||||
@@ -34,7 +36,7 @@ fn set_auth_cookies(jar: CookieJar, token: &str, refresh_token: &str) -> CookieJ
|
||||
|
||||
let refresh = Cookie::build((REFRESH_TOKEN_COOKIE, refresh_token.to_string()))
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.secure(secure)
|
||||
.same_site(SameSite::Strict)
|
||||
.path("/api/v1/auth")
|
||||
.max_age(refresh_max_age.try_into().unwrap_or_else(|_| std::time::Duration::from_secs(86400).try_into().unwrap()))
|
||||
@@ -43,6 +45,13 @@ fn set_auth_cookies(jar: CookieJar, token: &str, refresh_token: &str) -> CookieJ
|
||||
jar.add(access).add(refresh)
|
||||
}
|
||||
|
||||
/// 检查是否为开发模式(Cookie Secure、CORS 等安全策略依据此判断)
|
||||
fn is_dev_mode() -> bool {
|
||||
std::env::var("ZCLAW_SAAS_DEV")
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 清除 auth cookies
|
||||
fn clear_auth_cookies(jar: CookieJar) -> CookieJar {
|
||||
jar.remove(Cookie::build(ACCESS_TOKEN_COOKIE).path("/api"))
|
||||
@@ -502,9 +511,40 @@ fn sha256_hex(input: &str) -> String {
|
||||
hex::encode(Sha256::digest(input.as_bytes()))
|
||||
}
|
||||
|
||||
/// POST /api/v1/auth/logout — 清除 auth cookies
|
||||
/// POST /api/v1/auth/logout — 撤销 refresh token 并清除 auth cookies
|
||||
pub async fn logout(
|
||||
State(state): State<AppState>,
|
||||
jar: CookieJar,
|
||||
) -> (CookieJar, axum::http::StatusCode) {
|
||||
// 尝试从 cookie 中获取 refresh token 并撤销
|
||||
if let Some(refresh_cookie) = jar.get(REFRESH_TOKEN_COOKIE) {
|
||||
let token = refresh_cookie.value();
|
||||
if let Ok(claims) = verify_token_skip_expiry(token, state.jwt_secret.expose_secret()) {
|
||||
if claims.token_type == "refresh" {
|
||||
if let Some(jti) = claims.jti {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
// 标记 refresh token 为已使用(等效于撤销/黑名单)
|
||||
let result = sqlx::query(
|
||||
"UPDATE refresh_tokens SET used_at = $1 WHERE jti = $2 AND used_at IS NULL"
|
||||
)
|
||||
.bind(&now).bind(&jti)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(r) => {
|
||||
if r.rows_affected() > 0 {
|
||||
tracing::info!(account_id = %claims.sub, jti = %jti, "Refresh token revoked on logout");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(jti = %jti, error = %e, "Failed to revoke refresh token on logout");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(clear_auth_cookies(jar), axum::http::StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user