mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat(auth): 改进 OAuth 授权流程和界面
- 将 JSON payload 改为 form 参数提交 - 使用 sessionStorage 存储 state 防止丢失 - 优化 OAuth 回调页面的 UI 和动画效果 - 修复状态检查逻辑并立即取消监听防止重复触发
This commit is contained in:
@@ -358,19 +358,19 @@ pub async fn oauth_exchange_code(
|
||||
println!("[OAuth] 换取令牌 - code: {}, platform_id: {}, callback_url: {}", code, platform_id, callback_url);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let payload = serde_json::json!({
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": platform_id,
|
||||
"client_secret": platform_secret,
|
||||
"redirect_uri": callback_url
|
||||
});
|
||||
let params = [
|
||||
("grant_type", "authorization_code"),
|
||||
("code", &code),
|
||||
("client_id", &platform_id),
|
||||
("client_secret", &platform_secret),
|
||||
("redirect_uri", &callback_url),
|
||||
];
|
||||
|
||||
println!("[OAuth] 请求体:{:?}", payload);
|
||||
println!("[OAuth] 请求参数:{:?}", params);
|
||||
|
||||
let response = client
|
||||
.post("https://sectl.top/api/oauth/token")
|
||||
.json(&payload)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
@@ -380,9 +380,7 @@ pub async fn oauth_exchange_code(
|
||||
let response_text = response.text().await.map_err(|e| format!("Failed to read response: {}", e))?;
|
||||
println!("[OAuth] 响应内容:{}", response_text);
|
||||
|
||||
let status_success = response_text.is_empty() || !response_text.contains("error");
|
||||
|
||||
if !status_success {
|
||||
if !response_text.is_empty() && response_text.contains("error") {
|
||||
return Ok(IpcResponse::error(&response_text));
|
||||
}
|
||||
|
||||
|
||||
@@ -124,42 +124,99 @@ async fn handle_oauth_callback(
|
||||
<meta charset="UTF-8">
|
||||
<title>OAuth 授权完成</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: #e4e4e4;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
padding: 48px 40px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(74, 222, 128, 0.4);
|
||||
animation: scaleIn 0.5s ease-out 0.2s both;
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: white;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 24px;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
color: #a0a0a0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.success { color: #4ade80; }
|
||||
.error { color: #f87171; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="success">✓ 授权成功</h1>
|
||||
<div class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>授权成功</h1>
|
||||
<p>请返回 SecScore 应用查看登录结果</p>
|
||||
<p style="margin-top: 16px; font-size: 12px; opacity: 0.7;">此窗口可以关闭</p>
|
||||
<p class="hint">此窗口可以关闭</p>
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
@@ -177,42 +234,99 @@ async fn handle_oauth_callback(
|
||||
<meta charset="UTF-8">
|
||||
<title>OAuth 授权失败</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: #e4e4e4;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
padding: 48px 40px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(248, 113, 113, 0.4);
|
||||
animation: scaleIn 0.5s ease-out 0.2s both;
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: white;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 24px;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
color: #a0a0a0;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.success { color: #4ade80; }
|
||||
.error { color: #f87171; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="error">✗ 授权失败</h1>
|
||||
<div class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>授权失败</h1>
|
||||
<p>请返回 SecScore 应用查看错误信息</p>
|
||||
<p style="margin-top: 16px; font-size: 12px; opacity: 0.7;">此窗口可以关闭</p>
|
||||
<p class="hint">此窗口可以关闭</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -32,7 +32,10 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const callbackUnlistenRef = useRef<UnlistenFn | null>(null)
|
||||
const expectedStateRef = useRef<string | null>(null)
|
||||
// 使用 sessionStorage 存储 state,防止组件重新渲染导致丢失
|
||||
const getExpectedState = () => sessionStorage.getItem("oauth_expected_state")
|
||||
const setExpectedState = (state: string) => sessionStorage.setItem("oauth_expected_state", state)
|
||||
const clearExpectedState = () => sessionStorage.removeItem("oauth_expected_state")
|
||||
|
||||
const getOAuthConfig = (): OAuthConfig | null => {
|
||||
const api = (window as any).api
|
||||
@@ -69,10 +72,11 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
|
||||
|
||||
if (result.code) {
|
||||
console.log("[OAuth] 授权码:", result.code)
|
||||
console.log("[OAuth] State:", result.state, "期望:", expectedStateRef.current)
|
||||
console.log("[OAuth] State:", result.state, "期望:", getExpectedState())
|
||||
|
||||
// 验证 state 防止 CSRF
|
||||
if (expectedStateRef.current && result.state !== expectedStateRef.current) {
|
||||
const expectedState = getExpectedState()
|
||||
if (expectedState && result.state !== expectedState) {
|
||||
message.error("安全验证失败:state 不匹配")
|
||||
setLoading(false)
|
||||
return
|
||||
@@ -103,7 +107,7 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
|
||||
}
|
||||
|
||||
await api.oauthStopCallbackServer()
|
||||
expectedStateRef.current = null
|
||||
clearExpectedState()
|
||||
onSuccess(userRes.data)
|
||||
onClose()
|
||||
}
|
||||
@@ -120,6 +124,11 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
|
||||
const unlisten = await listen<OAuthCallbackResult>("oauth-callback", async (event) => {
|
||||
console.log("[OAuth] Event listener 收到 payload:", event.payload)
|
||||
if (event.payload) {
|
||||
// 立即取消监听,防止重复触发
|
||||
if (callbackUnlistenRef.current) {
|
||||
callbackUnlistenRef.current()
|
||||
callbackUnlistenRef.current = null
|
||||
}
|
||||
await handleOAuthCallback(event.payload)
|
||||
}
|
||||
})
|
||||
@@ -137,7 +146,7 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
|
||||
callbackUnlistenRef.current = null
|
||||
}
|
||||
}
|
||||
}, []) // handleOAuthCallback 在依赖数组外,使用 ref 存储
|
||||
}, [])
|
||||
|
||||
const handleOAuthLogin = async () => {
|
||||
const config = getOAuthConfig()
|
||||
@@ -163,8 +172,8 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
|
||||
// 生成随机 state 防止 CSRF
|
||||
const state = generateRandomState()
|
||||
console.log("[OAuth] 生成 state:", state)
|
||||
expectedStateRef.current = state
|
||||
console.log("[OAuth] state 已设置:", expectedStateRef.current)
|
||||
setExpectedState(state)
|
||||
console.log("[OAuth] state 已设置:", getExpectedState())
|
||||
|
||||
const urlRes = await api.oauthGetAuthorizationUrl(config.platform_id, callbackUrl, state)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user