fix: P1/P2 优化 - OAuth验证 + API响应 + 缓存击穿 + Webhook关闭
P1 - OAuth auth_url origin 验证: - 添加 validateOAuthUrl() 函数验证 OAuth URL origin - 仅允许同源或可信 OAuth 提供商 - LoginPage 和 ProfileSecurityPage 调用前验证 P2 - API 响应运行时类型验证: - 添加 isApiResponse() 运行时验证函数 - parseJsonResponse 验证响应结构完整性 P2 - 缓存击穿防护 (singleflight): - AuthMiddleware.isJTIBlacklisted 使用 singleflight.Group - 防止 L1 miss 时并发请求同时打 L2 P2 - Webhook 服务优雅关闭: - WebhookService 添加 Shutdown() 方法 - 服务器关闭时等待 worker 完成 - main.go 集成 shutdown 调用
This commit is contained in:
@@ -31,7 +31,8 @@ import type { RcFile } from 'antd/es/upload'
|
||||
import dayjs from 'dayjs'
|
||||
import { useAuth } from '@/app/providers/auth-context'
|
||||
import { getErrorMessage } from '@/lib/errors'
|
||||
import { parseOAuthCallbackHash } from '@/lib/auth/oauth'
|
||||
import { parseOAuthCallbackHash, validateOAuthUrl } from '@/lib/auth/oauth'
|
||||
import { getDeviceFingerprint } from '@/lib/device-fingerprint'
|
||||
import { PageLayout, ContentCard } from '@/components/layout'
|
||||
import { PageHeader } from '@/components/common'
|
||||
import { getAuthCapabilities } from '@/services/auth'
|
||||
@@ -198,6 +199,11 @@ export function ProfileSecurityPage() {
|
||||
totp_code: values.totp_code?.trim() || undefined,
|
||||
})
|
||||
|
||||
// 验证 OAuth URL origin 防止开放重定向攻击
|
||||
if (!validateOAuthUrl(result.auth_url)) {
|
||||
throw new Error('Invalid OAuth authorization URL')
|
||||
}
|
||||
|
||||
setBindVisible(false)
|
||||
setActiveProvider(null)
|
||||
bindSocialForm.resetFields()
|
||||
@@ -306,11 +312,8 @@ export function ProfileSecurityPage() {
|
||||
// If "remember device" is checked, trust the current device
|
||||
if (totpRememberDevice) {
|
||||
try {
|
||||
const stored = localStorage.getItem('device_fingerprint')
|
||||
if (stored) {
|
||||
const deviceInfo = JSON.parse(stored)
|
||||
await trustDeviceByDeviceId(deviceInfo.device_id, '30d')
|
||||
}
|
||||
const deviceInfo = getDeviceFingerprint()
|
||||
await trustDeviceByDeviceId(deviceInfo.device_id, '30d')
|
||||
} catch {
|
||||
// Non-critical: device trust failed, but TOTP was enabled
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
|
||||
import { useAuth } from '@/app/providers/auth-context'
|
||||
import { AuthLayout } from '@/layouts'
|
||||
import { buildOAuthCallbackReturnTo, sanitizeAuthRedirect } from '@/lib/auth/oauth'
|
||||
import { buildOAuthCallbackReturnTo, sanitizeAuthRedirect, validateOAuthUrl } from '@/lib/auth/oauth'
|
||||
import { getErrorMessage, isFormValidationError } from '@/lib/errors'
|
||||
import { getDeviceFingerprint } from '@/lib/device-fingerprint'
|
||||
import {
|
||||
getAuthCapabilities,
|
||||
getOAuthAuthorizationUrl,
|
||||
@@ -52,34 +53,6 @@ type SmsCodeFormValues = {
|
||||
code: string
|
||||
}
|
||||
|
||||
// 构建设备指纹
|
||||
function buildDeviceFingerprint(): { device_id: string; device_name: string; device_browser: string; device_os: string } {
|
||||
const ua = navigator.userAgent
|
||||
let browser = 'Unknown'
|
||||
let os = 'Unknown'
|
||||
|
||||
if (ua.includes('Chrome')) browser = 'Chrome'
|
||||
else if (ua.includes('Firefox')) browser = 'Firefox'
|
||||
else if (ua.includes('Safari')) browser = 'Safari'
|
||||
else if (ua.includes('Edge')) browser = 'Edge'
|
||||
|
||||
if (ua.includes('Windows')) os = 'Windows'
|
||||
else if (ua.includes('Mac')) os = 'macOS'
|
||||
else if (ua.includes('Linux')) os = 'Linux'
|
||||
else if (ua.includes('Android')) os = 'Android'
|
||||
else if (ua.includes('iOS')) os = 'iOS'
|
||||
|
||||
// 使用随机ID作为设备唯一标识
|
||||
const deviceId = `${browser}-${os}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
|
||||
return {
|
||||
device_id: deviceId,
|
||||
device_name: `${browser} on ${os}`,
|
||||
device_browser: browser,
|
||||
device_os: os,
|
||||
}
|
||||
}
|
||||
|
||||
export function LoginPage() {
|
||||
const [activeTab, setActiveTab] = useState('password')
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -165,6 +138,10 @@ export function LoginPage() {
|
||||
provider,
|
||||
buildOAuthCallbackReturnTo(redirect),
|
||||
)
|
||||
// 验证 OAuth URL origin 防止开放重定向攻击
|
||||
if (!validateOAuthUrl(result.auth_url)) {
|
||||
throw new Error('Invalid OAuth authorization URL')
|
||||
}
|
||||
window.location.assign(result.auth_url)
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error, '启动第三方登录失败'))
|
||||
@@ -175,9 +152,7 @@ export function LoginPage() {
|
||||
const handlePasswordLogin = useCallback(async (values: LoginFormValues) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const deviceInfo = buildDeviceFingerprint()
|
||||
// Store device info for "remember device" feature on TOTP enable
|
||||
localStorage.setItem('device_fingerprint', JSON.stringify(deviceInfo))
|
||||
const deviceInfo = getDeviceFingerprint()
|
||||
const tokenBundle = await loginByPassword({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
|
||||
Reference in New Issue
Block a user