const DEFAULT_DINGTALK_RETURN_ORIGINS = ['http://192.168.58.20:4320'] type DingtalkRequestLike = { headers?: { get?: (name: string) => string | null | undefined } nextUrl?: { origin?: string } } function normalizeHttpOrigin(value: unknown): string | null { const raw = String(value ?? '').trim() if (!raw) return null try { const parsed = new URL(raw) if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { return null } if (parsed.username || parsed.password || parsed.pathname !== '/' || parsed.search || parsed.hash) { return null } return parsed.origin } catch { return null } } function parseConfiguredOrigins(value: string | undefined): string[] { return String(value || '') .split(',') .map((item) => normalizeHttpOrigin(item)) .filter((item): item is string => Boolean(item)) } function getFirstHeaderValue(request: DingtalkRequestLike, name: string): string { const raw = request.headers?.get?.(name) || '' return raw.split(',')[0].trim() } function getProtocolFromForwardedHeader(request: DingtalkRequestLike): 'http' | 'https' | null { const protocol = getFirstHeaderValue(request, 'x-forwarded-proto').replace(/:$/, '').toLowerCase() if (protocol === 'http' || protocol === 'https') { return protocol } return null } /** * 钉钉必须回到公网完成 OAuth;这里仅定义 OAuth 完成后允许交接会话的内网入口。 * * 生产环境通过 DINGTALK_INTERNAL_RETURN_ORIGINS 显式配置。未配置时仅保留当前稳定内网入口, * 防止把 state 中任意地址当成跳转目标。 */ export function getDingtalkReturnOrigins(): string[] { const configured = parseConfiguredOrigins(process.env.DINGTALK_INTERNAL_RETURN_ORIGINS) const candidates = configured.length > 0 ? configured : DEFAULT_DINGTALK_RETURN_ORIGINS return Array.from(new Set(candidates)) } export function getTrustedDingtalkReturnOrigin(value: unknown): string | null { const origin = normalizeHttpOrigin(value) if (!origin) return null return getDingtalkReturnOrigins().includes(origin) ? origin : null } /** * Next.js 在设置 NEXTAUTH_URL 后可能把 request.nextUrl 固定解析为公网地址。 * 内网交接必须优先使用反向代理实际传入的 Host + Protocol,再做精确白名单校验。 */ export function getTrustedDingtalkReturnOriginFromRequest(request: DingtalkRequestLike): string | null { // 本地代理明确保留 Host;X-Forwarded-Host 只作为没有 Host 时的兼容兜底, // 避免把客户端可伪造的转发头当成首选来源。 const host = getFirstHeaderValue(request, 'host') || getFirstHeaderValue(request, 'x-forwarded-host') if (host) { const protocol = getProtocolFromForwardedHeader(request) || 'http' const trustedOrigin = getTrustedDingtalkReturnOrigin(`${protocol}://${host}`) if (trustedOrigin) { return trustedOrigin } } return getTrustedDingtalkReturnOrigin(request.nextUrl?.origin) }