mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
fix(auth): forward session cookies through the Next API proxy
Login Set-Cookie was dropped by Next rewrites, so non-admin sessions never stuck and every later API looked unauthenticated. Proxy JSON APIs in proxy.ts, copy Set-Cookie, send 401 to login and 403 to /403.
This commit is contained in:
@@ -34,10 +34,8 @@ func setCacheService(s contracts.CacheService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDB(ctx context.Context) *gorm.DB {
|
func getDB(ctx context.Context) *gorm.DB {
|
||||||
if c, ok := ctx.(*core.Context); ok && c != nil {
|
if s, err := core.InjectFrom[contracts.DBService](ctx); err == nil && s != nil {
|
||||||
if s, err := core.Inject[contracts.DBService](c); err == nil && s != nil {
|
return s.DB(ctx)
|
||||||
return s.DB(ctx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dbMu.RLock()
|
dbMu.RLock()
|
||||||
s := dbSvc
|
s := dbSvc
|
||||||
@@ -49,10 +47,8 @@ func getDB(ctx context.Context) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCache(ctx context.Context) contracts.CacheService {
|
func getCache(ctx context.Context) contracts.CacheService {
|
||||||
if c, ok := ctx.(*core.Context); ok && c != nil {
|
if s, err := core.InjectFrom[contracts.CacheService](ctx); err == nil && s != nil {
|
||||||
if s, err := core.Inject[contracts.CacheService](c); err == nil && s != nil {
|
return s
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cacheMu.RLock()
|
cacheMu.RLock()
|
||||||
s := cacheSvc
|
s := cacheSvc
|
||||||
|
|||||||
@@ -119,6 +119,26 @@ function isPublicAuthRequest(url?: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initiateLogin(currentPath: string): Promise<never> {
|
||||||
|
if (
|
||||||
|
currentPath.startsWith('/login') ||
|
||||||
|
currentPath.startsWith('/register') ||
|
||||||
|
currentPath.startsWith('/callback') ||
|
||||||
|
currentPath.startsWith('/403')
|
||||||
|
) {
|
||||||
|
return Promise.reject(new UnauthorizedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
sessionStorage.setItem('redirect_after_login', currentPath);
|
||||||
|
const loginUrl = new URL('/login', window.location.origin);
|
||||||
|
loginUrl.searchParams.set('callbackUrl', currentPath);
|
||||||
|
window.location.href = loginUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<never>(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 响应拦截器
|
* 响应拦截器
|
||||||
* 处理 API 响应和统一错误处理
|
* 处理 API 响应和统一错误处理
|
||||||
@@ -152,13 +172,13 @@ apiClient.interceptors.response.use(
|
|||||||
return Promise.reject(cancelError);
|
return Promise.reject(cancelError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 401:未登录。公开认证接口只把错误交给页面;已登录态下的误报不得清 cookie 跳登录页。 */
|
/* 401:未登录 → 登录页。登录/注册/人机校验接口把错误交给表单。 */
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
const message = error.response.data?.error_msg || '未登录';
|
const message = error.response.data?.error_msg || '未登录';
|
||||||
if (!isPublicAuthRequest(error.config?.url)) {
|
if (isPublicAuthRequest(error.config?.url)) {
|
||||||
toast.error(message, { id: 'unauthorized-error' });
|
return Promise.reject(new UnauthorizedError(message));
|
||||||
}
|
}
|
||||||
return Promise.reject(new UnauthorizedError(message));
|
return initiateLogin(window.location.pathname + window.location.search);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 403:已登录但权限不足,进入独立 403 页,不清 cookie。 */
|
/* 403:已登录但权限不足,进入独立 403 页,不清 cookie。 */
|
||||||
|
|||||||
+98
-3
@@ -73,9 +73,94 @@ if (typeof setInterval !== 'undefined') {
|
|||||||
}, 60000);
|
}, 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backendOrigin(): string {
|
||||||
|
return process.env.WAVELET_BACKEND_URL || 'http://localhost:8000';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultipart(request: NextRequest): boolean {
|
||||||
|
return (request.headers.get('content-type') || '').includes(
|
||||||
|
'multipart/form-data',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDocumentNavigation(request: NextRequest): boolean {
|
||||||
|
return request.headers.get('sec-fetch-dest') === 'document';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyApiToBackend(request: NextRequest): Promise<NextResponse> {
|
||||||
|
const { pathname, search } = request.nextUrl;
|
||||||
|
const url = `${backendOrigin()}${pathname}${search}`;
|
||||||
|
const headers = new Headers();
|
||||||
|
request.headers.forEach((value, key) => {
|
||||||
|
const lower = key.toLowerCase();
|
||||||
|
if (
|
||||||
|
lower === 'host' ||
|
||||||
|
lower === 'connection' ||
|
||||||
|
lower === 'content-length' ||
|
||||||
|
lower === 'transfer-encoding'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
headers.set(key, value);
|
||||||
|
});
|
||||||
|
const host = request.headers.get('host');
|
||||||
|
if (host) {
|
||||||
|
headers.set('x-forwarded-host', host);
|
||||||
|
}
|
||||||
|
headers.set(
|
||||||
|
'x-forwarded-proto',
|
||||||
|
request.nextUrl.protocol.replace(':', '') || 'http',
|
||||||
|
);
|
||||||
|
|
||||||
|
const method = request.method.toUpperCase();
|
||||||
|
const init: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
redirect: 'manual',
|
||||||
|
};
|
||||||
|
if (method !== 'GET' && method !== 'HEAD') {
|
||||||
|
init.body = request.body;
|
||||||
|
Object.assign(init, { duplex: 'half' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstream = await fetch(url, init);
|
||||||
|
|
||||||
|
if (isDocumentNavigation(request)) {
|
||||||
|
if (upstream.status === 401) {
|
||||||
|
const loginUrl = new URL('/login', request.url);
|
||||||
|
loginUrl.searchParams.set('callbackUrl', '/home');
|
||||||
|
return NextResponse.redirect(loginUrl);
|
||||||
|
}
|
||||||
|
if (upstream.status === 403) {
|
||||||
|
return NextResponse.redirect(new URL('/403', request.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseHeaders = new Headers();
|
||||||
|
upstream.headers.forEach((value, key) => {
|
||||||
|
const lower = key.toLowerCase();
|
||||||
|
if (lower === 'set-cookie' || lower === 'transfer-encoding') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
responseHeaders.set(key, value);
|
||||||
|
});
|
||||||
|
const out = new NextResponse(upstream.body, {
|
||||||
|
status: upstream.status,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
const cookies =
|
||||||
|
typeof upstream.headers.getSetCookie === 'function'
|
||||||
|
? upstream.headers.getSetCookie()
|
||||||
|
: [];
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
out.headers.append('set-cookie', cookie);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 代理主函数 ==================== */
|
/* ==================== 代理主函数 ==================== */
|
||||||
|
|
||||||
export function proxy(request: NextRequest) {
|
export async function proxy(request: NextRequest) {
|
||||||
const { pathname, search } = request.nextUrl;
|
const { pathname, search } = request.nextUrl;
|
||||||
const sessionCookieName =
|
const sessionCookieName =
|
||||||
process.env.WAVELET_SESSION_COOKIE_NAME || 'wavelet_session_id';
|
process.env.WAVELET_SESSION_COOKIE_NAME || 'wavelet_session_id';
|
||||||
@@ -86,7 +171,7 @@ export function proxy(request: NextRequest) {
|
|||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* API 请求:速率限制后放行 */
|
/* API 请求:速率限制后反代,并原样回写 Set-Cookie(rewrites 会丢登录 Cookie) */
|
||||||
if (pathname.startsWith('/api/')) {
|
if (pathname.startsWith('/api/')) {
|
||||||
const rateLimitEnabled = process.env.WAVELET_RATE_LIMIT_ENABLED === 'true';
|
const rateLimitEnabled = process.env.WAVELET_RATE_LIMIT_ENABLED === 'true';
|
||||||
|
|
||||||
@@ -108,7 +193,17 @@ export function proxy(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NextResponse.next();
|
if (isMultipart(request)) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await proxyApiToBackend(request);
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error_msg: '无法连接到服务器', data: null },
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面请求:公共路由放行 */
|
/* 页面请求:公共路由放行 */
|
||||||
|
|||||||
Reference in New Issue
Block a user