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

如何正确配置Next-Auth与Keycloak的会话同步及过期策略

Next-Auth + Keycloak 实现会话同步与超时控制

完全可以实现你要的3小时无活动登出、1天强制登出需求,并且保持两端会话同步。核心是让Next-Auth的会话生命周期与Keycloak的会话配置对齐,同时通过钩子函数和主动检查同步两端状态,以下是具体方案:

一、先理清Keycloak配置和Next-Auth的关联

  • Keycloak后台的会话闲置时长(路径:Sessions > Session Settings > Session Idle Timeout)对应3小时无活动登出,最大会话时长(Session Max Lifetime)对应1天强制登出。这两个设置直接决定Keycloak颁发的access_token和refresh_token有效期,是同步两端会话的基础。
  • Next-Auth默认只会利用Keycloak返回的令牌初始化自身会话,但不会主动监听Keycloak的会话过期事件,这就是之前两端会话不同步的原因。

二、核心配置步骤

1. 对齐Next-Auth会话与Keycloak令牌规则

修改[...nextauth].js配置,采用JWT策略管理会话,并实现令牌自动刷新逻辑:

import NextAuth from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";

export default NextAuth({
  providers: [
    KeycloakProvider({
      clientId: process.env.KEYCLOAK_CLIENT_ID,
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
      issuer: process.env.KEYCLOAK_ISSUER,
      // 必须请求offline_access scope,才能拿到可刷新的refresh_token
      authorization: {
        params: {
          scope: "openid profile email offline_access",
        },
      },
    }),
  ],
  session: {
    // 使用JWT策略,依赖Keycloak令牌而非Next-Auth自有cookie
    strategy: "jwt",
    // 会话最大时长设为1天(86400秒),和Keycloak的Session Max Lifetime保持一致
    maxAge: 86400,
  },
  callbacks: {
    async jwt({ token, user, account }) {
      // 首次登录时,保存Keycloak的令牌及过期时间
      if (account && user) {
        return {
          ...token,
          accessToken: account.access_token,
          refreshToken: account.refresh_token,
          expiresAt: account.expires_at * 1000, // 转成毫秒格式
        };
      }

      // 令牌未过期(提前1分钟判断),直接返回
      if (Date.now() < token.expiresAt - 60000) {
        return token;
      }

      // 令牌即将过期,调用Keycloak接口刷新
      try {
        const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            client_id: process.env.KEYCLOAK_CLIENT_ID,
            client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
            grant_type: "refresh_token",
            refresh_token: token.refreshToken,
          }),
        });

        const refreshedTokens = await response.json();

        if (!response.ok) throw refreshedTokens;

        // 更新令牌信息
        return {
          ...token,
          accessToken: refreshedTokens.access_token,
          refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
          expiresAt: Date.now() + refreshedTokens.expires_in * 1000,
        };
      } catch (error) {
        // 刷新失败,标记令牌错误,后续触发登出
        return { ...token, error: "RefreshTokenError" };
      }
    },
    async session({ session, token }) {
      // 将令牌状态同步到session,供前端使用
      session.user.accessToken = token.accessToken;
      session.error = token.error;

      // 如果令牌刷新失败,直接标记会话过期
      if (token.error === "RefreshTokenError") {
        session.expires = new Date(0).toISOString();
      }

      return session;
    },
    async signOut({ token }) {
      // 登出时调用Keycloak接口,清除Keycloak端会话
      await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/logout`, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          client_id: process.env.KEYCLOAK_CLIENT_ID,
          client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
          refresh_token: token.refreshToken,
        }),
      });
      return true;
    },
  },
});

2. 处理被动过期的会话同步

当Keycloak会话因闲置或达到最大时长过期时,refresh_token会失效,此时Next-Auth的JWT回调会触发刷新失败,session回调会标记会话过期。前端可以通过useSession监听会话状态,一旦发现过期,自动触发登出,而signOut回调会同步清除Keycloak端的会话。

3. 前端实现无活动超时监听

在前端监听用户的交互事件(鼠标移动、键盘输入等),如果3小时无活动,主动调用signOut同步两端会话:

// 示例:在_app.js中添加监听逻辑
import { useSession, signOut } from "next-auth/react";
import { useEffect } from "react";

function MyApp({ Component, pageProps }) {
  const { data: session } = useSession();

  useEffect(() => {
    let timeoutId;

    // 重置超时器
    const resetTimeout = () => {
      clearTimeout(timeoutId);
      // 3小时=10800000毫秒
      timeoutId = setTimeout(() => {
        session && signOut({ callbackUrl: "/login" });
      }, 10800000);
    };

    // 绑定用户活动事件
    window.addEventListener("mousemove", resetTimeout);
    window.addEventListener("keydown", resetTimeout);
    window.addEventListener("scroll", resetTimeout);

    // 初始化超时器
    resetTimeout();

    // 组件卸载时清除监听和超时器
    return () => {
      clearTimeout(timeoutId);
      window.removeEventListener("mousemove", resetTimeout);
      window.removeEventListener("keydown", resetTimeout);
      window.removeEventListener("scroll", resetTimeout);
    };
  }, [session]);

  return <Component {...pageProps} />;
}

export default MyApp;

三、关键注意事项

  • Keycloak配置必须正确:将Session Idle Timeout设为3小时,Session Max Lifetime设为1天,确保令牌有效期符合需求。
  • 必须请求offline_access scope:否则Keycloak不会颁发可重复使用的refresh_token,刷新逻辑会失效。
  • Next-Auth的session.maxAge要和Keycloak的最大会话时长保持一致:避免Next-Auth会话还在但Keycloak会话已过期的情况。
  • 前端监听是辅助,核心依赖Keycloak令牌刷新:即使前端没有触发无活动超时,当Keycloak会话过期后,Next-Auth刷新令牌失败也会自动同步登出。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 22:17:08