支持Authorization头认证
This commit is contained in:
@@ -262,7 +262,6 @@ export async function GET(request: NextRequest) {
|
|||||||
response.cookies.set('oidc_session', JSON.stringify(oidcSession), {
|
response.cookies.set('oidc_session', JSON.stringify(oidcSession), {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
maxAge: 600, // 10分钟
|
maxAge: 600, // 10分钟
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export async function GET(request: NextRequest) {
|
|||||||
response.cookies.set('oidc_state', state, {
|
response.cookies.set('oidc_state', state, {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
maxAge: 600, // 10分钟
|
maxAge: 600, // 10分钟
|
||||||
});
|
});
|
||||||
|
|||||||
+26
-12
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
|
/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { parseAuthInfo } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,21 @@ const STORAGE_TYPE =
|
|||||||
| 'kvrocks'
|
| 'kvrocks'
|
||||||
| undefined) || 'localstorage';
|
| undefined) || 'localstorage';
|
||||||
|
|
||||||
|
function buildLoginResponse(authToken?: string | null) {
|
||||||
|
const body: Record<string, unknown> = { 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(
|
async function generateSignature(
|
||||||
data: string,
|
data: string,
|
||||||
@@ -161,7 +177,7 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// 未配置 PASSWORD 时直接放行
|
// 未配置 PASSWORD 时直接放行
|
||||||
if (!envPassword) {
|
if (!envPassword) {
|
||||||
const response = NextResponse.json({ ok: true });
|
const response = buildLoginResponse();
|
||||||
|
|
||||||
// 清除可能存在的认证cookie
|
// 清除可能存在的认证cookie
|
||||||
response.cookies.set('auth', '', {
|
response.cookies.set('auth', '', {
|
||||||
@@ -169,7 +185,6 @@ export async function POST(req: NextRequest) {
|
|||||||
expires: new Date(0),
|
expires: new Date(0),
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -188,7 +203,6 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证成功,设置认证cookie
|
// 验证成功,设置认证cookie
|
||||||
const response = NextResponse.json({ ok: true });
|
|
||||||
const username = process.env.USERNAME || 'default';
|
const username = process.env.USERNAME || 'default';
|
||||||
const deviceInfo = getDeviceInfo(req);
|
const deviceInfo = getDeviceInfo(req);
|
||||||
const cookieValue = await generateAuthCookie(
|
const cookieValue = await generateAuthCookie(
|
||||||
@@ -198,6 +212,7 @@ export async function POST(req: NextRequest) {
|
|||||||
true,
|
true,
|
||||||
deviceInfo
|
deviceInfo
|
||||||
); // localstorage 模式包含 password
|
); // localstorage 模式包含 password
|
||||||
|
const response = buildLoginResponse(cookieValue);
|
||||||
const expires = new Date();
|
const expires = new Date();
|
||||||
expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期)
|
expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期)
|
||||||
|
|
||||||
@@ -255,7 +270,6 @@ export async function POST(req: NextRequest) {
|
|||||||
password === process.env.PASSWORD
|
password === process.env.PASSWORD
|
||||||
) {
|
) {
|
||||||
// 验证成功,设置认证cookie
|
// 验证成功,设置认证cookie
|
||||||
const response = NextResponse.json({ ok: true });
|
|
||||||
const deviceInfo = getDeviceInfo(req);
|
const deviceInfo = getDeviceInfo(req);
|
||||||
const cookieValue = await generateAuthCookie(
|
const cookieValue = await generateAuthCookie(
|
||||||
username,
|
username,
|
||||||
@@ -264,6 +278,7 @@ export async function POST(req: NextRequest) {
|
|||||||
false,
|
false,
|
||||||
deviceInfo
|
deviceInfo
|
||||||
); // 数据库模式不包含 password
|
); // 数据库模式不包含 password
|
||||||
|
const response = buildLoginResponse(cookieValue);
|
||||||
const expires = new Date();
|
const expires = new Date();
|
||||||
expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期)
|
expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期)
|
||||||
|
|
||||||
@@ -308,7 +323,6 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证成功,设置认证cookie
|
// 验证成功,设置认证cookie
|
||||||
const response = NextResponse.json({ ok: true });
|
|
||||||
const deviceInfo = getDeviceInfo(req);
|
const deviceInfo = getDeviceInfo(req);
|
||||||
const cookieValue = await generateAuthCookie(
|
const cookieValue = await generateAuthCookie(
|
||||||
username,
|
username,
|
||||||
@@ -317,16 +331,16 @@ export async function POST(req: NextRequest) {
|
|||||||
false,
|
false,
|
||||||
deviceInfo
|
deviceInfo
|
||||||
); // 数据库模式不包含 password
|
); // 数据库模式不包含 password
|
||||||
|
const response = buildLoginResponse(cookieValue);
|
||||||
const expires = new Date();
|
const expires = new Date();
|
||||||
expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期)
|
expires.setDate(expires.getDate() + 60); // 60天过期(Refresh Token 有效期)
|
||||||
|
|
||||||
response.cookies.set('auth', cookieValue, {
|
response.cookies.set('auth', cookieValue, {
|
||||||
path: '/',
|
path: '/',
|
||||||
expires,
|
expires,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
httpOnly: false, // 允许客户端访问
|
httpOnly: false, // 允许客户端访问
|
||||||
secure: process.env.NODE_ENV === 'production', // 生产环境强制 HTTPS
|
});
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Cookie已设置`);
|
console.log(`Cookie已设置`);
|
||||||
|
|
||||||
|
|||||||
+67
-27
@@ -1,36 +1,85 @@
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
// 从cookie获取认证信息 (服务端使用)
|
export type AuthInfo = {
|
||||||
export function getAuthInfoFromCookie(request: NextRequest): {
|
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
role?: 'owner' | 'admin' | 'user';
|
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');
|
const authCookie = request.cookies.get('auth');
|
||||||
|
|
||||||
if (!authCookie) {
|
if (!authCookie) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return parseAuthInfo(authCookie.value);
|
||||||
const decoded = decodeURIComponent(authCookie.value);
|
|
||||||
const authData = JSON.parse(decoded);
|
|
||||||
return authData;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从cookie获取认证信息 (客户端使用)
|
// 从cookie获取认证信息 (客户端使用)
|
||||||
export function getAuthInfoFromBrowserCookie(): {
|
export function getAuthInfoFromBrowserCookie(): AuthInfo | null {
|
||||||
password?: string;
|
|
||||||
username?: string;
|
|
||||||
signature?: string;
|
|
||||||
timestamp?: number;
|
|
||||||
role?: 'owner' | 'admin' | 'user';
|
|
||||||
} | null {
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -57,16 +106,7 @@ export function getAuthInfoFromBrowserCookie(): {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理可能的双重编码
|
return parseAuthInfo(authCookie);
|
||||||
let decoded = decodeURIComponent(authCookie);
|
|
||||||
|
|
||||||
// 如果解码后仍然包含 %,说明是双重编码,需要再次解码
|
|
||||||
if (decoded.includes('%')) {
|
|
||||||
decoded = decodeURIComponent(decoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
const authData = JSON.parse(decoded);
|
|
||||||
return authData;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user