R18-1/R18-3: 接入企业微信入口绑定与免登回退
这个提交包含在:
+82
-4
@@ -29,6 +29,85 @@ function getRequestOrigin(request) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOriginValue(value) {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(trimmed).origin.replace(/\/+$/, "");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseAllowedOriginContext(config, request) {
|
||||
const allowedOrigins = new Set();
|
||||
const allowedHosts = new Set();
|
||||
|
||||
for (const item of String(config.appOrigin || "").split(",").map((value) => value.trim()).filter(Boolean)) {
|
||||
const normalized = normalizeOriginValue(item);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
allowedOrigins.add(normalized);
|
||||
try {
|
||||
allowedHosts.add(new URL(normalized).host);
|
||||
} catch {
|
||||
// ignore invalid app origin host
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of String(config.wecomAllowedReturnHosts || "").split(",").map((value) => value.trim()).filter(Boolean)) {
|
||||
allowedHosts.add(item);
|
||||
}
|
||||
|
||||
if (!allowedOrigins.size) {
|
||||
const fallbackOrigin = normalizeOriginValue(resolvePrimaryAppOrigin(config, request));
|
||||
if (fallbackOrigin) {
|
||||
allowedOrigins.add(fallbackOrigin);
|
||||
try {
|
||||
allowedHosts.add(new URL(fallbackOrigin).host);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { allowedOrigins, allowedHosts };
|
||||
}
|
||||
|
||||
function isRequestOriginAllowed(config, request) {
|
||||
const requestOrigin = getRequestOrigin(request);
|
||||
if (!requestOrigin) {
|
||||
return { allowed: true, requestOrigin: null, expectedOrigins: [] };
|
||||
}
|
||||
|
||||
const normalizedRequestOrigin = normalizeOriginValue(requestOrigin);
|
||||
if (!normalizedRequestOrigin) {
|
||||
return { allowed: false, requestOrigin, expectedOrigins: [] };
|
||||
}
|
||||
|
||||
const { allowedOrigins, allowedHosts } = parseAllowedOriginContext(config, request);
|
||||
if (allowedOrigins.has(normalizedRequestOrigin)) {
|
||||
return { allowed: true, requestOrigin: normalizedRequestOrigin, expectedOrigins: Array.from(allowedOrigins) };
|
||||
}
|
||||
|
||||
try {
|
||||
const host = new URL(normalizedRequestOrigin).host;
|
||||
if (allowedHosts.has(host)) {
|
||||
return { allowed: true, requestOrigin: normalizedRequestOrigin, expectedOrigins: Array.from(allowedOrigins) };
|
||||
}
|
||||
} catch {
|
||||
// ignore invalid host
|
||||
}
|
||||
|
||||
return { allowed: false, requestOrigin: normalizedRequestOrigin, expectedOrigins: Array.from(allowedOrigins) };
|
||||
}
|
||||
|
||||
function refreshCookieOptions(config) {
|
||||
return {
|
||||
httpOnly: true,
|
||||
@@ -361,16 +440,15 @@ async function authRoutes(fastify) {
|
||||
});
|
||||
}
|
||||
|
||||
const expectedOrigin = fastify.config.appOrigin;
|
||||
const requestOrigin = getRequestOrigin(request);
|
||||
if (expectedOrigin && requestOrigin && requestOrigin !== expectedOrigin) {
|
||||
const originCheck = isRequestOriginAllowed(fastify.config, request);
|
||||
if (!originCheck.allowed) {
|
||||
await fastify.prisma.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
actorId: session.employee_id,
|
||||
action: "AUTH_REFRESH_ORIGIN_MISMATCH",
|
||||
targetType: "employee",
|
||||
targetId: session.employee_id,
|
||||
extra: { expectedOrigin, requestOrigin },
|
||||
extra: { expectedOrigins: originCheck.expectedOrigins, requestOrigin: originCheck.requestOrigin },
|
||||
}),
|
||||
});
|
||||
throw new AppError({
|
||||
|
||||
在新工单中引用
屏蔽一个用户