diff --git a/app/api/dingtalk/login/route.ts b/app/api/dingtalk/login/route.ts index cabc05a..3caeee0 100644 --- a/app/api/dingtalk/login/route.ts +++ b/app/api/dingtalk/login/route.ts @@ -28,7 +28,7 @@ function isSafeHandoffNonce(value: unknown) { async function attachInternalSessionHandoff(source: NextResponse, state: unknown) { const data = await source.clone().json().catch(() => null) - if (!source.ok || !data?.success || data?.needCompleteProfile || !data?.user?.id) { + if (!data) { return source } @@ -43,6 +43,24 @@ async function attachInternalSessionHandoff(source: NextResponse, state: unknown return source } + // 首次补资料和授权失败都还没有可交接的内网会话,但弹窗必须能把结果送回原内网页面。 + // 目标地址只来自服务器保存的受信 handoff,绝不能回退为 postMessage('*')。 + if (!source.ok || !data?.success || data?.needCompleteProfile || !data?.user?.id) { + const response = NextResponse.json( + { + ...data, + handoffTargetOrigin: handoff.returnOrigin, + handoffNonce: nonce, + }, + { + status: source.status, + headers: { 'Cache-Control': 'no-store' }, + }, + ) + appendSetCookies(response, getSetCookies(source)) + return response + } + const ticket = await createDingtalkSessionTicket({ userId: String(data.user.id), redirectPath: handoff.redirectPath, @@ -84,8 +102,12 @@ async function attachInternalSessionHandoff(source: NextResponse, state: unknown */ export async function POST(request: NextRequest) { + let state: unknown = '' + try { - const { code, state } = await request.json() + const body = await request.json() + const { code } = body + state = body?.state if (!code) { return NextResponse.json( @@ -101,12 +123,13 @@ export async function POST(request: NextRequest) { return attachInternalSessionHandoff(response, state) } catch (error: any) { console.error('钉钉登录失败:', error) - return NextResponse.json( + const response = NextResponse.json( { success: false, message: error.message || '登录失败,请稍后再试', }, { status: 500 }, ) + return attachInternalSessionHandoff(response, state) } } diff --git a/app/dingtalk-callback/page.tsx b/app/dingtalk-callback/page.tsx index efbc4ed..5087e4c 100644 --- a/app/dingtalk-callback/page.tsx +++ b/app/dingtalk-callback/page.tsx @@ -94,7 +94,13 @@ function DingtalkCallbackContent() { const data = await response.json() if (!response.ok || !data.success) { - throw new Error(data.message || '登录失败') + const loginError = new Error(data.message || '登录失败') as Error & { + handoffTargetOrigin?: string + handoffNonce?: string + } + loginError.handoffTargetOrigin = data?.handoffTargetOrigin + loginError.handoffNonce = data?.handoffNonce + throw loginError } // 检查是否需要完善信息 @@ -113,7 +119,7 @@ function DingtalkCallbackContent() { userInfo: data.user, nonce, }, - window.location.origin, + data.handoffTargetOrigin || window.location.origin, ) window.close() } else if (window.parent !== window) { @@ -124,7 +130,7 @@ function DingtalkCallbackContent() { userInfo: data.user, nonce, }, - window.location.origin, + data.handoffTargetOrigin || window.location.origin, ) } else { router.push(`/complete-profile?userInfo=${userInfoParam}`) @@ -195,16 +201,25 @@ function DingtalkCallbackContent() { setTimeout(() => { if (window.opener) { // 新窗口模式:通知打开窗口的父页面 + const targetOrigin = error?.handoffTargetOrigin + if (!targetOrigin || error?.handoffNonce !== parseDingtalkLoginState(state).nonce) { + // 若连受信 handoff 都无法确认,保留错误页给用户看,不能关闭弹窗后让内网页面无提示地卡住。 + return + } window.opener.postMessage( { type: 'dingtalk_login_error', message: error.message, nonce: parseDingtalkLoginState(state).nonce }, - window.location.origin, + targetOrigin, ) window.close() } else if (window.parent !== window) { // iframe模式:通知父窗口(兼容旧代码) + const targetOrigin = error?.handoffTargetOrigin + if (!targetOrigin || error?.handoffNonce !== parseDingtalkLoginState(state).nonce) { + return + } window.parent.postMessage( { type: 'dingtalk_login_error', message: error.message, nonce: parseDingtalkLoginState(state).nonce }, - window.location.origin, + targetOrigin, ) } else { router.push('/login')