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

基于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:登录方式的核心问题分析

你的登录流程存在两个关键问题,导致中间件不识别前端已认证:

  1. 跨域会话不共享:前端SPA和中间件是不同域名,当你在新标签页完成OIDC流程后,会话cookie(connect.sid)被存在http://middleware域名下。但前端后续调用中间件API时,浏览器默认不会跨域携带这个cookie,所以中间件找不到对应的会话,自然认为用户未认证,就会重定向到Keycloak。
  2. 前端存储的id_token未被中间件认可:你把id_token存在前端localStorage,但中间件的keycloak.protect()是基于自身会话存储的keycloak-token来判断认证状态的,根本不会读取前端的localStorage内容。

修复方案

要让中间件识别前端的认证状态,需要调整两点:

  • 配置跨域cookie传递:
    1. 在express-session配置中添加cookie: { sameSite: 'none', secure: true }(HTTPS环境),或者sameSite: 'lax'(开发环境HTTP),同时前端发起请求时要设置withCredentials: true(比如用axios的话就是axios.defaults.withCredentials = true)。这样浏览器才会在跨域请求中携带connect.sid cookie。
    2. 确保中间件配置了CORS,允许http://frontend的跨域请求,并且允许credentials。比如用cors包:
      const cors = require('cors')
      app.use(cors({ origin: 'http://frontend', credentials: true }))
      
  • 调整登录流程的逻辑:
    其实你不需要把id_token传回前端再存localStorage,因为中间件已经通过会话维护了认证状态。前端只要在后续请求中携带connect.sid cookie,中间件的keycloak.protect()就会自动识别。如果前端需要用户信息,可以单独加一个受保护的API接口(比如/api/userinfo),让前端调用这个接口获取用户信息,而不是通过postMessage传递。

问题2:登出的完整实现方式

仅清除浏览器cookie是不够的,因为Keycloak服务器端也保存了用户的会话,需要完整的登出流程:

  1. 前端触发登出请求:调用中间件的/logout接口(你已经配置了keycloak.middleware({ logout: '/logout' })),这个接口会让Keycloak服务器销毁用户会话,同时清除中间件的会话cookie。
  2. 前端清理本地存储:登出成功后,前端要清除localStorage中的id_token(如果之前存了),并刷新页面或重置应用状态。
  3. 注意跨域登出的处理:如果是跨域场景,前端调用/logout时同样需要设置withCredentials: true,确保浏览器携带connect.sid cookie,这样中间件才能找到对应的会话并通知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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:18:08