You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

83 lines
2.4 KiB

import type { Permission } from "@/lib/constants/auth";
import { forbidden } from "@/modules/auth/errors";
import type { AuthContext } from "@/modules/auth/rbac";
import { verifyServiceTokenAsync } from "@/modules/auth/service-token";
export type ServiceAuthContext = Extract<AuthContext, { type: "service" }>;
function parsePermissionsHeader(raw: string): Permission[] {
return raw
.split(",")
.map((p) => p.trim())
.filter(Boolean) as Permission[];
}
function parseVerifiedServiceAuth(request: Request): ServiceAuthContext | null {
const authType = request.headers.get("x-auth-type");
if (authType !== "service") {
return null;
}
const customerId = request.headers.get("x-customer-id");
if (!customerId) {
throw forbidden("缺少客户标识");
}
const verified = request.headers.get("x-service-verified");
const permissionsRaw = request.headers.get("x-permissions") ?? "";
if (verified === "true" || permissionsRaw.length > 0) {
return {
type: "service",
customerId,
permissions: parsePermissionsHeader(permissionsRaw),
};
}
return null;
}
function extractBearerFromRequest(request: Request): string | null {
const forwarded = request.headers.get("x-bearer-token")?.trim();
if (forwarded) {
return forwarded;
}
const authHeader = request.headers.get("authorization");
if (authHeader?.startsWith("Bearer ")) {
return authHeader.slice(7).trim();
}
return null;
}
/** 从 middleware 注入的请求头解析宿主鉴权上下文(含 DB Token 二次校验) */
export async function parseServiceAuth(
request: Request,
): Promise<ServiceAuthContext> {
const fastPath = parseVerifiedServiceAuth(request);
if (fastPath) {
return fastPath;
}
const bearerToken = extractBearerFromRequest(request);
if (!bearerToken) {
throw forbidden("需要宿主 Service Token");
}
const customerId = request.headers.get("x-customer-id")?.trim() || undefined;
const verified = await verifyServiceTokenAsync(bearerToken, customerId);
return {
type: "service",
customerId: verified.customerId,
permissions: verified.permissions,
};
}
/** 校验请求体 customer_id 与 token 租户一致 */
export function assertCustomerMatch(
auth: ServiceAuthContext,
bodyCustomerId: string,
): void {
if (auth.customerId !== bodyCustomerId) {
throw forbidden("customer_id 与令牌租户不匹配");
}
}