You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 14 App目录项目集成SAML身份认证技术求助

Next.js 14 App Router 下实现 SAML SSO 身份认证

核心依赖选择

使用 @node-saml/node-saml(轻量无依赖的现代SAML库)和 iron-session(适配Next.js App Router的安全会话存储方案),先安装依赖:

npm install @node-saml/node-saml iron-session iron-session/next

1. 配置 SAML 参数

在 lib/saml/config.ts 中存储IDP和SP的配置信息(这些值需从组织IDP管理员处获取):

import { SamlConfig } from "@node-saml/node-saml";

export const samlConfig: SamlConfig = {
  // Service Provider(SP)配置
  entityID: "https://your-app-domain.com/api/auth/saml/metadata",
  assertionConsumerServiceURL: "https://your-app-domain.com/api/auth/saml/callback",
  privateKey: process.env.SP_PRIVATE_KEY as string, // 你的SP私钥
  // Identity Provider(IDP)配置
  entryPoint: process.env.IDP_SSO_URL as string, // IDP登录URL
  issuer: process.env.IDP_ENTITY_ID as string, // IDP实体ID
  cert: process.env.IDP_PUBLIC_CERT as string, // IDP公钥证书
  signatureAlgorithm: "sha256",
};

2. 实现 SAML 相关 API 路由

登录发起路由(app/api/auth/saml/login/route.ts)

生成SAML认证请求并跳转到IDP登录页面:

import { NextResponse } from "next/server";
import { SamlInstance } from "@node-saml/node-saml";
import { samlConfig } from "@/lib/saml/config";

export async function GET() {
  const saml = new SamlInstance(samlConfig);
  const loginUrl = saml.createLoginRequestUrl();
  return NextResponse.redirect(loginUrl);
}

ACS回调路由(app/api/auth/saml/callback/route.ts)

接收IDP返回的SAML断言,验证通过后创建用户会话:

import { NextResponse } from "next/server";
import { SamlInstance } from "@node-saml/node-saml";
import { samlConfig } from "@/lib/saml/config";
import { getIronSession } from "iron-session/next";

// 会话配置
const sessionOptions = {
  password: process.env.IRON_SESSION_PASSWORD as string,
  cookieName: "saml-session",
  cookieOptions: {
    secure: process.env.NODE_ENV === "production",
  },
};

export async function POST(request: Request) {
  const saml = new SamlInstance(samlConfig);
  const formData = await request.formData();
  const samlResponse = formData.get("SAMLResponse") as string;

  try {
    const profile = await saml.validatePostResponseAsync(samlResponse);
    // 从profile中提取用户信息(根据IDP返回字段调整)
    const user = {
      id: profile.nameID,
      email: profile.attributes.email[0],
      name: profile.attributes.name[0],
    };

    // 创建会话
    const session = await getIronSession(request, NextResponse.next(), sessionOptions);
    session.user = user;
    await session.save();

    return NextResponse.redirect(new URL("/", request.url));
  } catch (err) {
    console.error("SAML验证失败:", err);
    return NextResponse.redirect(new URL("/auth/error", request.url));
  }
}

注销路由(app/api/auth/saml/logout/route.ts)

处理单点注销,清除本地会话并跳转到IDP注销页面:

import { NextResponse } from "next/server";
import { SamlInstance } from "@node-saml/node-saml";
import { samlConfig } from "@/lib/saml/config";
import { getIronSession } from "iron-session/next";
import { sessionOptions } from "../callback/route";

export async function GET(request: Request) {
  const session = await getIronSession(request, NextResponse.next(), sessionOptions);
  const saml = new SamlInstance(samlConfig);
  
  // 生成IDP注销URL
  const logoutUrl = saml.createLogoutRequestUrl({
    nameID: session.user?.id,
  });

  // 清除会话
  session.destroy();
  await session.save();

  return NextResponse.redirect(logoutUrl);
}

3. 路由保护(Middleware)

创建 middleware.ts,拦截未登录用户访问受保护路由:

import { NextRequest, NextResponse } from "next/server";
import { getIronSession } from "iron-session/next";
import { sessionOptions } from "./app/api/auth/saml/callback/route";

export async function middleware(request: NextRequest) {
  const session = await getIronSession(request, NextResponse.next(), sessionOptions);
  
  // 排除公开路由
  const publicPaths = ["/api/auth/saml/login", "/api/auth/saml/callback", "/auth/error"];
  const isPublic = publicPaths.some(path => request.nextUrl.pathname.startsWith(path));

  if (!session.user && !isPublic) {
    return NextResponse.redirect(new URL("/api/auth/saml/login", request.url));
  }

  return NextResponse.next();
}

// 指定需要拦截的路由
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

4. 在服务器组件中获取用户信息

在服务器组件(如 app/page.tsx)中直接读取会话并渲染用户信息:

import { getIronSession } from "iron-session/next";
import { sessionOptions } from "./api/auth/saml/callback/route";

export default async function Home() {
  const session = await getIronSession(sessionOptions);
  const user = session.user;

  return (
    <div>
      {user ? (
        <>
          <h1>欢迎回来, {user.name}!</h1>
          <p>邮箱: {user.email}</p>
          <a href="/api/auth/saml/logout">退出登录</a>
        </>
      ) : (
        <a href="/api/auth/saml/login">登录</a>
      )}
    </div>
  );
}

关键注意事项

  • 确保 .env.local 中正确配置环境变量:SP私钥、IDP证书、IRON_SESSION_PASSWORD(需至少32位字符)。
  • 若IDP需要SP元数据,可创建 /api/auth/saml/metadata/route.ts 路由,返回 saml.generateMetadata() 的结果。
  • SAML通常要求生产环境使用HTTPS,本地开发可通过ngrok等工具暴露HTTPS端口测试。

内容的提问来源于stack exchange,提问作者ilbassa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 13:16:16