Next.js+NextAuth+Keycloak集成无法获取JWT令牌对接Quarkus后端问题
问题分析与解决方案
疑问解答
- 这类问题非常常见,NextAuth默认不会主动暴露Keycloak颁发的原始JWT,而是生成自身的会话令牌,这是导致你无法获取Keycloak JWT的核心原因之一。
- 完全可行且是业界常见架构:Keycloak作为统一身份提供商(IDP),NextAuth负责前端认证流程,Quarkus后端通过验证Keycloak JWT实现接口鉴权,这套组合能很好支撑前后端分离的安全架构。
- 关键在于调整NextAuth配置以捕获并传递Keycloak JWT,同时确保前后端的令牌验证逻辑对齐,具体步骤如下:
步骤1:修改NextAuth配置以获取Keycloak JWT
在pages/api/auth/[...nextauth].js(或app/api/auth/[...nextauth]/route.js,对应Next.js 13+ App Router)中调整配置,重点关注scope设置和回调函数:
import NextAuth from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; export const authOptions = { secret: process.env.NEXTAUTH_SECRET, // 必须设置,从环境变量读取 providers: [ KeycloakProvider({ clientId: process.env.KEYCLOAK_CLIENT_ID, clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, issuer: process.env.KEYCLOAK_ISSUER, // 格式:https://your-keycloak-domain/realms/your-realm authorization: { params: { scope: "openid email profile offline_access", // 必须包含openid才能获取id_token }, }, }), ], // 自定义JWT回调,保存Keycloak的原始令牌 callbacks: { async jwt({ token, account }) { // 用户首次登录时,保存Keycloak的access_token和id_token if (account) { token.accessToken = account.access_token; token.idToken = account.id_token; token.refreshToken = account.refresh_token; token.expiresAt = account.expires_at * 1000; // 转换为毫秒 } // 检查令牌是否即将过期,可在此添加刷新逻辑 if (Date.now() < token.expiresAt) { return token; } // 令牌过期时刷新(可选,需实现刷新逻辑) return await refreshAccessToken(token); }, // 自定义Session回调,将Keycloak令牌暴露给前端 async session({ session, token }) { session.user.accessToken = token.accessToken; session.user.idToken = token.idToken; return session; }, }, }; // 可选:实现令牌刷新逻辑 async function refreshAccessToken(token) { 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, idToken: refreshedTokens.id_token, refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, expiresAt: Date.now() + refreshedTokens.expires_in * 1000, }; } catch (error) { return { ...token, error: "RefreshAccessTokenError", }; } } export default NextAuth(authOptions);
步骤2:前端传递JWT到Quarkus后端
在前端API请求中,从NextAuth的session中获取accessToken,并添加到请求头的Authorization字段:
import { useSession } from "next-auth/react"; async function fetchProtectedData() { const { data: session } = useSession(); if (!session?.user?.accessToken) { throw new Error("用户未登录"); } const response = await fetch("https://your-quarkus-backend/api/protected", { headers: { Authorization: `Bearer ${session.user.accessToken}`, }, }); if (!response.ok) { throw new Error("请求失败"); } return response.json(); }
步骤3:验证Quarkus后端配置
确保Quarkus的application.properties正确配置Keycloak OIDC验证:
# Keycloak OIDC配置 quarkus.oidc.auth-server-url=https://your-keycloak-domain/realms/your-realm quarkus.oidc.client-id=your-keycloak-client-id quarkus.oidc.credentials.secret=your-keycloak-client-secret # 若客户端为confidential类型需设置 # 保护API端点 quarkus.http.auth.permission.authenticated.paths=/api/** quarkus.http.auth.permission.authenticated.policy=authenticated
关键注意事项
- Keycloak客户端配置:确保客户端的
Access Type与NextAuth匹配:若NextAuth运行在服务器端,建议设为confidential;若纯前端部署,设为public。同时开启Direct Access Grants Enabled(可选,用于令牌刷新)。 - Scope必须包含openid:OIDC协议要求
openidscope才能颁发id_token和access_token,这是很多开发者容易遗漏的点。 - NextAuth Secret:必须设置
NEXTAUTH_SECRET环境变量,否则会话加密会出问题,导致令牌无法正确存储和传递。
内容的提问来源于stack exchange,提问作者Rekouwn
相关产品推荐
相关产品推荐

