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

Keycloak与Remix集成:身份校验逻辑的部署位置咨询

Remix + Keycloak OAuth 认证实现指南(基于默认笔记应用)

一、Keycloak 前置配置

  1. 在Keycloak控制台创建新客户端,核心配置:
    • 客户端类型:机密(Remix后端为可信服务,推荐此类型)
    • 有效重定向URI:http://localhost:3000/auth/callback(本地开发地址,生产环境替换为实际域名)
    • 授权流程:启用授权码模式
  2. 记录以下信息,存入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 项目基础配置

  1. 安装依赖:
    npm install @remix-run/node @remix-run/react remix cookie
    
  2. 创建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,部署方式如下:

  1. 安装包:
    npm install @keycloak/keycloak-js
    
  2. 创建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;
    }
    
  3. 在根路由中包裹应用:
    修改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模式下后端已处理认证流程,前端无需再引入该工具包。

六、测试验证

  1. 启动Keycloak服务,确认客户端配置生效
  2. 启动Remix应用:npm run dev
  3. 访问http://localhost:3000,自动重定向到Keycloak登录页
  4. 登录后回调至Remix应用,令牌将存入Session与浏览器localStorage
  5. 访问笔记路由,确认未登录状态下会被重定向到登录页

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 10:19:18