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
相关产品推荐
相关产品推荐

