Keycloak与Remix集成:身份校验逻辑的部署位置咨询
Remix + Keycloak OAuth 认证实现指南(基于默认笔记应用)
一、Keycloak 前置配置
- 在Keycloak控制台创建新客户端,核心配置:
- 客户端类型:
机密(Remix后端为可信服务,推荐此类型) - 有效重定向URI:
http://localhost:3000/auth/callback(本地开发地址,生产环境替换为实际域名) - 授权流程:启用
授权码模式
- 客户端类型:
- 记录以下信息,存入Remix项目根目录的
.env文件:KEYCLOAK_CLIENT_ID=你的客户端ID KEYCLOAK_CLIENT_SECRET=你的客户端密钥 KEYCLOAK_ISSUER=http://你的Keycloak域名/auth/realms/你的领域名 KEYCLOAK_REDIRECT_URI=http://localhost:3000/auth/callback SESSION_SECRET=随机字符串(用于加密Session)
二、Remix 项目基础配置
- 安装依赖:
npm install @remix-run/node @remix-run/react remix cookie - 创建Session管理文件
app/session.server.ts:import { createCookieSessionStorage } from "@remix-run/node"; const sessionStorage = createCookieSessionStorage({ cookie: { name: "_remix_keycloak_session", sameSite: "lax", path: "/", httpOnly: true, secure: process.env.NODE_ENV === "production", secrets: [process.env.SESSION_SECRET || "dev-secret"], }, }); // 创建用户Session并存储令牌 export async function createUserSession(accessToken: string, idToken: string, redirectTo: string) { const session = await sessionStorage.getSession(); session.set("accessToken", accessToken); session.set("idToken", idToken); return redirect(redirectTo, { headers: { "Set-Cookie": await sessionStorage.commitSession(session), }, }); } // 获取当前用户Session export async function getUserSession(request: Request) { return sessionStorage.getSession(request.headers.get("Cookie")); } // 获取Access Token export async function getAccessToken(request: Request) { const session = await getUserSession(request); return session.get("accessToken"); }
三、后端路由实现(BFF模式)
1. 登录跳转路由 app/routes/login.tsx
负责将用户重定向到Keycloak认证页:
import type { LoaderFunction } from "@remix-run/node"; import { redirect } from "@remix-run/node"; export const loader: LoaderFunction = () => { const authUrl = new URL(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/auth`); authUrl.searchParams.set("client_id", process.env.KEYCLOAK_CLIENT_ID!); authUrl.searchParams.set("redirect_uri", process.env.KEYCLOAK_REDIRECT_URI!); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("scope", "openid email profile"); return redirect(authUrl.toString()); }; export default function Login() { return <p>正在跳转到登录页面...</p>; }
2. 认证回调路由 app/routes/auth/callback.tsx
处理Keycloak返回的授权码,换取令牌并存入Session:
import type { LoaderFunction } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { createUserSession } from "~/session.server"; export const loader: LoaderFunction = async ({ request }) => { const url = new URL(request.url); const code = url.searchParams.get("code"); if (!code) { return redirect("/login?error=missing_code"); } // 向Keycloak请求令牌 const tokenResponse = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from(`${process.env.KEYCLOAK_CLIENT_ID}:${process.env.KEYCLOAK_CLIENT_SECRET}`).toString("base64")}`, }, body: new URLSearchParams({ code, redirect_uri: process.env.KEYCLOAK_REDIRECT_URI!, grant_type: "authorization_code", }), }); const tokenData = await tokenResponse.json(); if (!tokenData.access_token || !tokenData.id_token) { return redirect("/login?error=invalid_token"); } // 存储令牌并重定向到主页 return createUserSession(tokenData.access_token, tokenData.id_token, "/"); };
四、前端令牌同步与路由保护
1. 根路由同步令牌到浏览器存储 app/root.tsx
在应用初始化时,将Session中的令牌同步到localStorage:
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "@remix-run/react"; import type { LoaderFunction } from "@remix-run/node"; import { getAccessToken } from "~/session.server"; import { useEffect } from "react"; export const loader: LoaderFunction = async ({ request }) => { const accessToken = await getAccessToken(request); return { accessToken }; }; export default function App() { const { accessToken } = useLoaderData<typeof loader>(); useEffect(() => { if (accessToken) { localStorage.setItem("keycloak_access_token", accessToken); } else { localStorage.removeItem("keycloak_access_token"); } }, [accessToken]); return ( <html lang="en"> <head> <Meta /> <Links /> </head> <body> <Outlet /> <ScrollRestoration /> <Scripts /> <LiveReload /> </body> </html> ); }
2. 保护笔记路由 app/routes/notes.$noteId.tsx
修改原有路由的Loader,增加认证检查:
import type { LoaderFunction } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { getAccessToken } from "~/session.server"; // 保留原有导入... export const loader: LoaderFunction = async ({ params, request }) => { const accessToken = await getAccessToken(request); if (!accessToken) { return redirect("/login"); } // 保留原有业务逻辑... const note = await getNote(params.noteId); if (!note) { throw new Response("Not Found", { status: 404 }); } return note; }; // 保留原有组件代码...
五、Keycloak JS工具包的部署位置
若需前端直接与Keycloak交互,可使用@keycloak/keycloak-js,部署方式如下:
- 安装包:
npm install @keycloak/keycloak-js - 创建Keycloak Provider组件
app/components/KeycloakProvider.tsx:import { KeycloakInstance, KeycloakProvider as KCProvider } from "@keycloak/keycloak-js"; import { createContext, useContext, useEffect, useState } from "react"; const KeycloakContext = createContext<KeycloakInstance | null>(null); export function KeycloakProvider({ children }: { children: React.ReactNode }) { const [keycloak, setKeycloak] = useState<KeycloakInstance | null>(null); const [initialized, setInitialized] = useState(false); useEffect(() => { import("@keycloak/keycloak-js").then(({ default: Keycloak }) => { const kc = new Keycloak({ url: process.env.KEYCLOAK_ISSUER?.replace("/auth/realms/你的领域名", "") || "", realm: "你的领域名", clientId: process.env.KEYCLOAK_CLIENT_ID || "", }); kc.init({ onLoad: "check-sso", silentCheckSsoRedirectUri: window.location.origin + "/silent-check-sso.html", }).then(() => { setKeycloak(kc); setInitialized(true); }); }); }, []); if (!initialized) { return <p>加载中...</p>; } return ( <KCProvider keycloak={keycloak}> <KeycloakContext.Provider value={keycloak}> {children} </KeycloakContext.Provider> </KCProvider> ); } export function useKeycloak() { const context = useContext(KeycloakContext); if (!context) { throw new Error("useKeycloak必须在KeycloakProvider内部使用"); } return context; } - 在根路由中包裹应用:
修改app/root.tsx的返回部分:import { KeycloakProvider } from "~/components/KeycloakProvider"; // ... return ( <html lang="en"> <head> <Meta /> <Links /> </head> <body> <KeycloakProvider> <Outlet /> </KeycloakProvider> <ScrollRestoration /> <Scripts /> <LiveReload /> </body> </html> );
注意:此方式与BFF模式二选一即可,BFF模式下后端已处理认证流程,前端无需再引入该工具包。
六、测试验证
- 启动Keycloak服务,确认客户端配置生效
- 启动Remix应用:
npm run dev - 访问
http://localhost:3000,自动重定向到Keycloak登录页 - 登录后回调至Remix应用,令牌将存入Session与浏览器localStorage
- 访问笔记路由,确认未登录状态下会被重定向到登录页
内容的提问来源于stack exchange,提问作者Tyrannogyna
相关产品推荐
相关产品推荐

