修复钉钉通知直达登录态写入
This commit is contained in:
parent
b93d3cccda
commit
c91143d975
@ -4,7 +4,7 @@ import {
|
||||
getAuthSessionCookieCandidates,
|
||||
resolveSecureAuthCookieFromRequest,
|
||||
} from '@/lib/auth-cookie'
|
||||
import { buildDingtalkOAuthUrl } from '@/lib/dingtalk-oauth'
|
||||
import { DINGTALK_DIRECT_LOGIN_SOURCE, buildDingtalkOAuthUrl } from '@/lib/dingtalk-oauth'
|
||||
import { getPublicAppOrigin } from '@/lib/public-app-url'
|
||||
import { normalizeInternalRedirectPath } from '@/lib/safe-redirect'
|
||||
|
||||
@ -36,5 +36,6 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.redirect(buildDingtalkOAuthUrl({
|
||||
redirectPath,
|
||||
callbackPath: '/dingtalk-callback',
|
||||
source: DINGTALK_DIRECT_LOGIN_SOURCE,
|
||||
}))
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { getPublicAppOrigin } from '@/lib/public-app-url'
|
||||
import { normalizeInternalRedirectPath } from '@/lib/safe-redirect'
|
||||
|
||||
const FALLBACK_DINGTALK_CLIENT_ID = 'ding1bs1gxwrcfu16x0f'
|
||||
export const DINGTALK_DIRECT_LOGIN_SOURCE = 'work_notice_direct'
|
||||
|
||||
export function getDingtalkOAuthClientId() {
|
||||
return (
|
||||
@ -12,11 +13,21 @@ export function getDingtalkOAuthClientId() {
|
||||
)
|
||||
}
|
||||
|
||||
export function encodeDingtalkOAuthState(redirectPath: unknown, origin = getPublicAppOrigin()) {
|
||||
return JSON.stringify({
|
||||
export function encodeDingtalkOAuthState(
|
||||
redirectPath: unknown,
|
||||
origin = getPublicAppOrigin(),
|
||||
options?: { source?: string },
|
||||
) {
|
||||
const payload: Record<string, string> = {
|
||||
origin,
|
||||
redirect: normalizeInternalRedirectPath(redirectPath),
|
||||
})
|
||||
}
|
||||
|
||||
if (options?.source) {
|
||||
payload.source = options.source
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
|
||||
export function parseDingtalkOAuthState(value: unknown) {
|
||||
@ -25,6 +36,7 @@ export function parseDingtalkOAuthState(value: unknown) {
|
||||
return {
|
||||
origin: '',
|
||||
redirectPath: '/',
|
||||
source: '',
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,11 +51,13 @@ export function parseDingtalkOAuthState(value: unknown) {
|
||||
return {
|
||||
origin: typeof data?.origin === 'string' ? data.origin : '',
|
||||
redirectPath: normalizeInternalRedirectPath(data?.redirect),
|
||||
source: typeof data?.source === 'string' ? data.source : '',
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
origin: '',
|
||||
redirectPath: '/',
|
||||
source: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,10 +66,12 @@ export function buildDingtalkOAuthUrl({
|
||||
redirectPath,
|
||||
callbackPath = '/api/dingtalk/callback',
|
||||
prompt,
|
||||
source,
|
||||
}: {
|
||||
redirectPath: unknown
|
||||
callbackPath?: string
|
||||
prompt?: string
|
||||
source?: string
|
||||
}) {
|
||||
const publicOrigin = getPublicAppOrigin()
|
||||
const redirectUrl = `${publicOrigin}${callbackPath}`
|
||||
@ -64,7 +80,7 @@ export function buildDingtalkOAuthUrl({
|
||||
client_id: getDingtalkOAuthClientId(),
|
||||
scope: 'openid',
|
||||
response_type: 'code',
|
||||
state: encodeDingtalkOAuthState(redirectPath, publicOrigin),
|
||||
state: encodeDingtalkOAuthState(redirectPath, publicOrigin, { source }),
|
||||
})
|
||||
|
||||
if (prompt) {
|
||||
|
||||
@ -5,10 +5,90 @@ import {
|
||||
getAuthSessionCookieCandidates,
|
||||
resolveSecureAuthCookieFromRequest,
|
||||
} from '@/lib/auth-cookie'
|
||||
import {
|
||||
DINGTALK_DIRECT_LOGIN_SOURCE,
|
||||
parseDingtalkOAuthState,
|
||||
} from '@/lib/dingtalk-oauth'
|
||||
import { getPublicAppOrigin } from '@/lib/public-app-url'
|
||||
import { normalizeInternalRedirectPath } from '@/lib/safe-redirect'
|
||||
|
||||
const API_CORS_ALLOWED_HEADERS =
|
||||
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-API-Key, Authorization, Cookie'
|
||||
|
||||
function buildLoginFallbackPath(redirectPath: unknown, error?: string) {
|
||||
const target = normalizeInternalRedirectPath(redirectPath)
|
||||
const url = new URL('/login', getPublicAppOrigin())
|
||||
url.searchParams.set('redirect', target)
|
||||
if (error) {
|
||||
url.searchParams.set('dingtalk_error', error)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
function getInternalDingtalkLoginUrl() {
|
||||
const port = process.env.PORT || '3000'
|
||||
return `http://127.0.0.1:${port}/api/dingtalk/login`
|
||||
}
|
||||
|
||||
function appendSetCookie(target: NextResponse, source: Response) {
|
||||
const setCookie = source.headers.get('set-cookie')
|
||||
if (setCookie) {
|
||||
target.headers.append('set-cookie', setCookie)
|
||||
}
|
||||
}
|
||||
|
||||
function getForwardedLoginHeaders(request: NextRequest) {
|
||||
const publicUrl = new URL(getPublicAppOrigin())
|
||||
return {
|
||||
'content-type': 'application/json',
|
||||
'x-forwarded-proto': publicUrl.protocol.replace(':', ''),
|
||||
'x-forwarded-host': publicUrl.host,
|
||||
'x-real-ip': request.headers.get('x-real-ip') || '',
|
||||
'x-forwarded-for': request.headers.get('x-forwarded-for') || '',
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDingtalkDirectLoginCallback(request: NextRequest) {
|
||||
const code = request.nextUrl.searchParams.get('code') || request.nextUrl.searchParams.get('authCode')
|
||||
const state = request.nextUrl.searchParams.get('state') || ''
|
||||
const parsedState = parseDingtalkOAuthState(state)
|
||||
|
||||
if (parsedState.source !== DINGTALK_DIRECT_LOGIN_SOURCE) {
|
||||
return null
|
||||
}
|
||||
|
||||
const publicOrigin = getPublicAppOrigin()
|
||||
const redirectPath = parsedState.redirectPath
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.redirect(buildLoginFallbackPath(redirectPath, '缺少钉钉授权码'))
|
||||
}
|
||||
|
||||
try {
|
||||
const loginResponse = await fetch(getInternalDingtalkLoginUrl(), {
|
||||
method: 'POST',
|
||||
headers: getForwardedLoginHeaders(request),
|
||||
body: JSON.stringify({ code, state }),
|
||||
cache: 'no-store',
|
||||
})
|
||||
const data = await loginResponse.json().catch(() => null)
|
||||
|
||||
if (!loginResponse.ok || !data?.success || data.needCompleteProfile) {
|
||||
return NextResponse.redirect(
|
||||
buildLoginFallbackPath(redirectPath, data?.message || '钉钉登录失败'),
|
||||
)
|
||||
}
|
||||
|
||||
const redirectResponse = NextResponse.redirect(new URL(redirectPath, publicOrigin))
|
||||
appendSetCookie(redirectResponse, loginResponse)
|
||||
redirectResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
return redirectResponse
|
||||
} catch (error) {
|
||||
console.error('[Dingtalk] 通知直达服务端登录失败:', error)
|
||||
return NextResponse.redirect(buildLoginFallbackPath(redirectPath, '钉钉登录失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// 注意:不在middleware中启动内存监控
|
||||
// middleware使用Edge Runtime,不支持Node.js API
|
||||
// 内存监控会在lib/memory-monitor.ts模块加载时自动启动(仅生产环境)
|
||||
@ -43,6 +123,13 @@ export async function middleware(request: NextRequest) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (pathname === '/dingtalk-callback') {
|
||||
const dingtalkDirectLoginResponse = await handleDingtalkDirectLoginCallback(request)
|
||||
if (dingtalkDirectLoginResponse) {
|
||||
return dingtalkDirectLoginResponse
|
||||
}
|
||||
}
|
||||
|
||||
// 处理需要认证的页面
|
||||
if (pathname === '/' || pathname.startsWith('/admin')) {
|
||||
const preferredSecureCookie = resolveSecureAuthCookieFromRequest(request)
|
||||
@ -136,5 +223,5 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/', '/admin/:path*', '/api/:path*'],
|
||||
matcher: ['/', '/admin/:path*', '/api/:path*', '/dingtalk-callback'],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user