From 29781d9d5a4959baf0a1c4f4448030b56304def0 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sun, 25 Jan 2026 16:07:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81Authorization=E5=A4=B4?= =?UTF-8?q?=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/auth/oidc/callback/route.ts | 1 - src/app/api/auth/oidc/login/route.ts | 1 - src/app/api/login/route.ts | 38 ++++++---- src/lib/auth.ts | 94 ++++++++++++++++++------- 4 files changed, 93 insertions(+), 41 deletions(-) diff --git a/src/app/api/auth/oidc/callback/route.ts b/src/app/api/auth/oidc/callback/route.ts index 024cfa8..2cb66f1 100644 --- a/src/app/api/auth/oidc/callback/route.ts +++ b/src/app/api/auth/oidc/callback/route.ts @@ -262,7 +262,6 @@ export async function GET(request: NextRequest) { response.cookies.set('oidc_session', JSON.stringify(oidcSession), { path: '/', httpOnly: true, - secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 600, // 10分钟 }); diff --git a/src/app/api/auth/oidc/login/route.ts b/src/app/api/auth/oidc/login/route.ts index c7e3bec..37703c8 100644 --- a/src/app/api/auth/oidc/login/route.ts +++ b/src/app/api/auth/oidc/login/route.ts @@ -47,7 +47,6 @@ export async function GET(request: NextRequest) { response.cookies.set('oidc_state', state, { path: '/', httpOnly: true, - secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 600, // 10分钟 }); diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts index a39a897..c326e0a 100644 --- a/src/app/api/login/route.ts +++ b/src/app/api/login/route.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console,@typescript-eslint/no-explicit-any */ import { NextRequest, NextResponse } from 'next/server'; +import { parseAuthInfo } from '@/lib/auth'; import { getConfig } from '@/lib/config'; import { db } from '@/lib/db'; import { @@ -21,6 +22,21 @@ const STORAGE_TYPE = | 'kvrocks' | undefined) || 'localstorage'; +function buildLoginResponse(authToken?: string | null) { + const body: Record = { ok: true }; + + if (authToken) { + body.token = authToken; + const authInfo = parseAuthInfo(authToken); + if (authInfo) { + const { password, ...rest } = authInfo; + body.auth = rest; + } + } + + return NextResponse.json(body); +} + // 生成签名 async function generateSignature( data: string, @@ -161,7 +177,7 @@ export async function POST(req: NextRequest) { // 未配置 PASSWORD 时直接放行 if (!envPassword) { - const response = NextResponse.json({ ok: true }); + const response = buildLoginResponse(); // 清除可能存在的认证cookie response.cookies.set('auth', '', { @@ -169,7 +185,6 @@ export async function POST(req: NextRequest) { expires: new Date(0), sameSite: 'lax', httpOnly: false, - secure: process.env.NODE_ENV === 'production', }); return response; @@ -188,7 +203,6 @@ export async function POST(req: NextRequest) { } // 验证成功,设置认证cookie - const response = NextResponse.json({ ok: true }); const username = process.env.USERNAME || 'default'; const deviceInfo = getDeviceInfo(req); const cookieValue = await generateAuthCookie( @@ -198,6 +212,7 @@ export async function POST(req: NextRequest) { true, deviceInfo ); // localstorage 模式包含 password + const response = buildLoginResponse(cookieValue); const expires = new Date(); expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期) @@ -255,7 +270,6 @@ export async function POST(req: NextRequest) { password === process.env.PASSWORD ) { // 验证成功,设置认证cookie - const response = NextResponse.json({ ok: true }); const deviceInfo = getDeviceInfo(req); const cookieValue = await generateAuthCookie( username, @@ -264,6 +278,7 @@ export async function POST(req: NextRequest) { false, deviceInfo ); // 数据库模式不包含 password + const response = buildLoginResponse(cookieValue); const expires = new Date(); expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期) @@ -308,7 +323,6 @@ export async function POST(req: NextRequest) { } // 验证成功,设置认证cookie - const response = NextResponse.json({ ok: true }); const deviceInfo = getDeviceInfo(req); const cookieValue = await generateAuthCookie( username, @@ -317,16 +331,16 @@ export async function POST(req: NextRequest) { false, deviceInfo ); // 数据库模式不包含 password + const response = buildLoginResponse(cookieValue); const expires = new Date(); expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期) - response.cookies.set('auth', cookieValue, { - path: '/', - expires, - sameSite: 'lax', - httpOnly: false, // 允许客户端访问 - secure: process.env.NODE_ENV === 'production', // 生产环境强制 HTTPS - }); + response.cookies.set('auth', cookieValue, { + path: '/', + expires, + sameSite: 'lax', + httpOnly: false, // 允许客户端访问 + }); console.log(`Cookie已设置`); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index c3c244d..f4b31b9 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,36 +1,85 @@ import { NextRequest } from 'next/server'; -// 从cookie获取认证信息 (服务端使用) -export function getAuthInfoFromCookie(request: NextRequest): { +export type AuthInfo = { password?: string; username?: string; signature?: string; timestamp?: number; role?: 'owner' | 'admin' | 'user'; -} | null { + tokenId?: string; + refreshToken?: string; + refreshExpires?: number; +}; + +function getAuthTokenFromHeader(value: string): string { + const trimmed = value.trim(); + if (!trimmed) { + return ''; + } + + const bearerMatch = trimmed.match(/^Bearer\s+(.+)$/i); + if (bearerMatch) { + return bearerMatch[1].trim(); + } + + const tokenMatch = trimmed.match(/^Token\s+(.+)$/i); + if (tokenMatch) { + return tokenMatch[1].trim(); + } + + return trimmed; +} + +export function parseAuthInfo(value?: string | null): AuthInfo | null { + if (!value) { + return null; + } + + let decoded = value; + + try { + decoded = decodeURIComponent(decoded); + } catch (error) { + decoded = value; + } + + if (decoded.includes('%')) { + try { + decoded = decodeURIComponent(decoded); + } catch (error) { + decoded = value; + } + } + + try { + return JSON.parse(decoded) as AuthInfo; + } catch (error) { + return null; + } +} + +// 从cookie获取认证信息 (服务端使用) +export function getAuthInfoFromCookie(request: NextRequest): AuthInfo | null { + const authHeader = request.headers.get('authorization'); + if (authHeader) { + const headerValue = getAuthTokenFromHeader(authHeader); + const headerAuthInfo = parseAuthInfo(headerValue); + if (headerAuthInfo) { + return headerAuthInfo; + } + } + const authCookie = request.cookies.get('auth'); if (!authCookie) { return null; } - try { - const decoded = decodeURIComponent(authCookie.value); - const authData = JSON.parse(decoded); - return authData; - } catch (error) { - return null; - } + return parseAuthInfo(authCookie.value); } // 从cookie获取认证信息 (客户端使用) -export function getAuthInfoFromBrowserCookie(): { - password?: string; - username?: string; - signature?: string; - timestamp?: number; - role?: 'owner' | 'admin' | 'user'; -} | null { +export function getAuthInfoFromBrowserCookie(): AuthInfo | null { if (typeof window === 'undefined') { return null; } @@ -57,16 +106,7 @@ export function getAuthInfoFromBrowserCookie(): { return null; } - // 处理可能的双重编码 - let decoded = decodeURIComponent(authCookie); - - // 如果解码后仍然包含 %,说明是双重编码,需要再次解码 - if (decoded.includes('%')) { - decoded = decodeURIComponent(decoded); - } - - const authData = JSON.parse(decoded); - return authData; + return parseAuthInfo(authCookie); } catch (error) { return null; }