feat(auth): 实现 OAuth PKCE 安全流程并优化错误处理

添加 PKCE code_verifier 和 code_challenge 支持以增强 OAuth 安全性
重构 OAuth 回调处理逻辑,添加全局锁防止重复处理
优化错误处理和信息打印,增加调试日志
移除不必要的 setTimeout 包装导出功能
格式化代码以提高可读性
This commit is contained in:
Yukino_fox
2026-04-12 17:01:32 +08:00
parent a2f82cb869
commit 98afd446c9
7 changed files with 188 additions and 79 deletions
+61 -5
View File
@@ -81,6 +81,7 @@ pub struct OAuthIntrospectResponse {
pub struct OAuthAuthorizationUrlResponse {
pub url: String,
pub state: String,
pub code_verifier: String,
}
fn get_iv_hex() -> String {
@@ -325,6 +326,29 @@ pub async fn auth_clear_all(
}
}
/// 生成 PKCE code_verifier
fn generate_code_verifier() -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
let random_bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
base64::Engine::encode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
&random_bytes,
)
}
/// 生成 PKCE code_challenge (S256)
fn generate_code_challenge(verifier: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
let result = hasher.finalize();
base64::Engine::encode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
&result,
)
}
#[tauri::command]
pub async fn oauth_get_authorization_url(
platform_id: String,
@@ -341,16 +365,22 @@ pub async fn oauth_get_authorization_url(
)
});
// 生成 PKCE 参数
let code_verifier = generate_code_verifier();
let code_challenge = generate_code_challenge(&code_verifier);
let url = format!(
"https://sectl.top/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&state={}",
"https://sectl.top/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&state={}&code_challenge={}&code_challenge_method=S256",
platform_id,
urlencoding::encode(&callback_url),
urlencoding::encode(&state)
urlencoding::encode(&state),
urlencoding::encode(&code_challenge)
);
Ok(IpcResponse::success(OAuthAuthorizationUrlResponse {
url,
state,
code_verifier,
}))
}
@@ -360,6 +390,7 @@ pub async fn oauth_exchange_code(
platform_id: String,
platform_secret: String,
callback_url: String,
code_verifier: String,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<OAuthTokenResponse>, String> {
println!(
@@ -375,6 +406,7 @@ pub async fn oauth_exchange_code(
"client_id": &platform_id,
"client_secret": &platform_secret,
"redirect_uri": &callback_url,
"code_verifier": &code_verifier,
});
println!("[OAuth] 请求参数:{:?}", payload);
@@ -485,6 +517,11 @@ pub async fn oauth_get_user_info(
access_token: String,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<OAuthUserInfo>, String> {
println!(
"[OAuth] 获取用户信息 - access_token: {}...",
&access_token[..20.min(access_token.len())]
);
let state_guard = state.read();
let client = &state_guard.http_client;
@@ -495,17 +532,36 @@ pub async fn oauth_get_user_info(
.await
.map_err(|e| format!("Request failed: {}", e))?;
println!("[OAuth] 用户信息响应状态: {}", response.status());
if !response.status().is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Ok(IpcResponse::error(&error_text));
println!("[OAuth] 用户信息响应错误: {}", error_text);
// 尝试解析 JSON 错误,提取 error_description 或 error
let error_message =
if let Ok(json_err) = serde_json::from_str::<serde_json::Value>(&error_text) {
json_err
.get("error_description")
.or_else(|| json_err.get("error"))
.and_then(|v| v.as_str())
.unwrap_or(&error_text)
.to_string()
} else {
error_text
};
return Ok(IpcResponse::error(&error_message));
}
let user_info: OAuthUserInfo = response
.json()
let response_text = response
.text()
.await
.map_err(|e| format!("Failed to read response: {}", e))?;
println!("[OAuth] 用户信息响应内容: {}", response_text);
let user_info: OAuthUserInfo = serde_json::from_str(&response_text)
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(IpcResponse::success(user_info))
+11 -4
View File
@@ -1567,7 +1567,9 @@ pub async fn apply_offline_backfill(
}
requested_runs_by_rule
.entry(item.rule_id)
.and_modify(|value| *value = (*value + capped).min(AUTO_SCORE_BACKFILL_MAX_RUNS_PER_RULE))
.and_modify(|value| {
*value = (*value + capped).min(AUTO_SCORE_BACKFILL_MAX_RUNS_PER_RULE)
})
.or_insert(capped);
}
if requested_runs_by_rule.is_empty() {
@@ -1610,7 +1612,8 @@ pub async fn apply_offline_backfill(
changed = true;
for _ in 0..replay_runs {
let stats = execute_rule(&conn, rule, &execution_batches, ExecutionMode::Backfill).await?;
let stats =
execute_rule(&conn, rule, &execution_batches, ExecutionMode::Backfill).await?;
result.applied_runs += 1;
result.affected_students += stats.affected_students;
result.created_events += stats.created_events;
@@ -1682,7 +1685,9 @@ async fn execute_rule(
if let Some(max_runs) = rule.execution.max_runs_per_day {
let today_runs = execution_batches
.iter()
.filter(|batch| batch.rule_id == rule.id && !batch.rolled_back && is_same_utc_day(&batch.run_at))
.filter(|batch| {
batch.rule_id == rule.id && !batch.rolled_back && is_same_utc_day(&batch.run_at)
})
.count() as i64;
if today_runs >= max_runs {
return Ok(RuleExecutionStats::default());
@@ -1700,7 +1705,9 @@ async fn execute_rule(
let mut daily_score_delta_used: i64 = execution_batches
.iter()
.filter(|batch| batch.rule_id == rule.id && !batch.rolled_back && is_same_utc_day(&batch.run_at))
.filter(|batch| {
batch.rule_id == rule.id && !batch.rolled_back && is_same_utc_day(&batch.run_at)
})
.map(|batch| batch.score_delta_total.abs())
.sum();