基于keycloak-nodejs-connect的前端与中间件认证交互问题
背景与架构配置
先梳理下你的系统架构和核心配置:
- 前端(frontend):由NGINX提供静态资源的SPA,地址为
http://frontend - Keycloak服务器:运行在
http://keycloak - 中间件(middleware):NodeJS应用,作为frontend的API端点(地址
http://middleware),使用keycloak-nodejs-connect保护API接口
中间件核心配置
const session = require('express-session') const memoryStore = new session.MemoryStore() app.use(session({ secret: 'mySecret', resave: false, saveUninitialized: true, store: memoryStore })) const keycloak = require('./auth/keycloak') app.use(keycloak.middleware({ logout: '/logout' }))
auth/keycloak.js配置
const keycloakConfig = { 'realm': 'realmName', 'auth-server-url': `http://keycloak/auth`, 'ssl-required': 'external', 'resource': 'clientName', 'credentials': { 'secret': 'aaaaaaaa-bbbb-cccc-dddd-1a49c9dfbbef' } } module.exports = new Keycloak({store: memoryStore, responseType: 'code'}, keycloakConfig)
登录流程实现
当用户点击frontend的/login链接时,frontend会打开新标签页调用http://middleware/login,中间件对应代码:
app.get('/login', keycloak.protect(), (req, res) => { const keycloakTokens = JSON.parse(req.session['keycloak-token']) const userInfo = JSON.stringify(decodeToken(keycloakTokens)) res.status(200).send(`<html><head> <script type="text/javascript"> window.opener.postMessage(${userInfo}, 'http://frontend') window.close() </script> </head></html>`) }) const decodeToken = ({id_token}) => { const payload = JSON.parse(atob(id_token.split('.')[1])) return payload }
设计思路:完成OIDC流程后,中间件返回响应,让新标签页通过postMessage向父窗口发送id_token的JWT内容,frontend将该信息保存到localStorage中。
假设验证
假设1:中间件通过cookie中的connect.sid会话来验证用户是否保持认证状态,该假设是否准确?
问题
问题1
基于上述配置,该登录方式是否正确?目前发现中间件的其他受保护路由会重定向到Keycloak,说明中间件未识别frontend已认证。
问题2
这种场景下登出该如何实现?仅清除浏览器cookie即可吗?
解答
假设1验证:准确,但有前提
没错,你的假设完全正确。keycloak-nodejs-connect依赖express-session来存储用户的认证状态,connect.sid是express-session生成的会话ID cookie,中间件通过这个cookie找到对应的会话,再从会话中读取keycloak-token来验证用户是否已认证。不过这里有个关键前提:会话cookie是和中间件域名(http://middleware)绑定的,前端SPA在http://frontend域名下,默认无法直接携带这个cookie访问中间件的受保护路由。
问题1:登录方式的核心问题分析
你的登录流程存在两个关键问题,导致中间件不识别前端已认证:
- 跨域会话不共享:前端SPA和中间件是不同域名,当你在新标签页完成OIDC流程后,会话cookie(
connect.sid)被存在http://middleware域名下。但前端后续调用中间件API时,浏览器默认不会跨域携带这个cookie,所以中间件找不到对应的会话,自然认为用户未认证,就会重定向到Keycloak。 - 前端存储的id_token未被中间件认可:你把id_token存在前端localStorage,但中间件的
keycloak.protect()是基于自身会话存储的keycloak-token来判断认证状态的,根本不会读取前端的localStorage内容。
修复方案
要让中间件识别前端的认证状态,需要调整两点:
- 配置跨域cookie传递:
- 在express-session配置中添加
cookie: { sameSite: 'none', secure: true }(HTTPS环境),或者sameSite: 'lax'(开发环境HTTP),同时前端发起请求时要设置withCredentials: true(比如用axios的话就是axios.defaults.withCredentials = true)。这样浏览器才会在跨域请求中携带connect.sidcookie。 - 确保中间件配置了CORS,允许
http://frontend的跨域请求,并且允许credentials。比如用cors包:const cors = require('cors') app.use(cors({ origin: 'http://frontend', credentials: true }))
- 在express-session配置中添加
- 调整登录流程的逻辑:
其实你不需要把id_token传回前端再存localStorage,因为中间件已经通过会话维护了认证状态。前端只要在后续请求中携带connect.sidcookie,中间件的keycloak.protect()就会自动识别。如果前端需要用户信息,可以单独加一个受保护的API接口(比如/api/userinfo),让前端调用这个接口获取用户信息,而不是通过postMessage传递。
问题2:登出的完整实现方式
仅清除浏览器cookie是不够的,因为Keycloak服务器端也保存了用户的会话,需要完整的登出流程:
- 前端触发登出请求:调用中间件的
/logout接口(你已经配置了keycloak.middleware({ logout: '/logout' })),这个接口会让Keycloak服务器销毁用户会话,同时清除中间件的会话cookie。 - 前端清理本地存储:登出成功后,前端要清除localStorage中的id_token(如果之前存了),并刷新页面或重置应用状态。
- 注意跨域登出的处理:如果是跨域场景,前端调用
/logout时同样需要设置withCredentials: true,确保浏览器携带connect.sidcookie,这样中间件才能找到对应的会话并通知Keycloak登出。
举个简单的登出实现例子:
- 中间件不需要额外代码,因为
keycloak.middleware已经处理了/logout - 前端代码:
async function logout() { try { await fetch('http://middleware/logout', { method: 'GET', credentials: 'include' // 携带cookie }) localStorage.removeItem('id_token') window.location.href = '/' // 回到首页 } catch (err) { console.error('Logout failed:', err) } }
内容的提问来源于stack exchange,提问作者cyberjar09

