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

解决Keycloak Node.js适配器弃用:Passport+OIDC配置求助

问题:Passport + OpenID Client 集成 Keycloak 后受保护路由失效

我此前使用已弃用的 keycloak-connect 搭建 Express 脚手架,现在改用 Passport + OpenID Client 适配 Keycloak。注册、登录接口功能正常,但受保护路由无法正常工作。附上当前代码,恳请提供 Passport.js 的正确配置模板或分步修复指南。


当前代码

主文件(app.ts)

import express, {
    Express,
    Request,
    Response
} from 'express';
import 'reflect-metadata';
import bodyparser from 'body-parser';
import userRouter from './api/routes/userRoutes';
import expressSession from 'express-session';
import cookieParser from 'cookie-parser'
import connection from './database/connection';
import cors from 'cors';
import { corsOptions } from './config/corsOptions';
import { credentials } from './api/middlewares/credentials';
import { Issuer, Strategy } from 'openid-client';
import passport from 'passport';
import config from './config';
import KeycloakStrategy from "@exlinc/keycloak-passport";

//GETTING PORT FROM .ENV FILE:
const PORT = config.port || 3000;
const app: Express = express();

var memoryStore = new expressSession.MemoryStore();
app.use(
    expressSession({
    secret: 'another_long_secret',
    resave: false,
    saveUninitialized: true,
    store: memoryStore
    })
);

app.use(passport.initialize());
app.use(passport.authenticate('session'));


Issuer.discover('http://localhost:8080/realms/Demo').then(function (oidcIssuer) {
    var client = new oidcIssuer.Client({
        client_id: 'keycloak_practice',
        client_secret: 'hkvogjo7jziNV3X4hR2rUILQikNKHpSL',
        redirect_uris: ['http://localhost:8000/private'],
        response_types: ['code'],
    })

    passport.use('oidc', new Strategy({client}, (tokenSet, userinfo, done)=>{
        return done(null, tokenSet.claims());
    })
)
});
passport.serializeUser(function(user, done) {
    done(null, user);
  });
passport.deserializeUser(function(user, done) {
    done(null, user);
});

app.use(credentials)
app.use(cors(corsOptions));
app.use(express.json());
app.use((bodyparser.urlencoded({ extended: true })));
app.use(cookieParser());

//CONNECTING TO DATABASE:
connection();

app.use('/user', userRouter);

//CONNECTION TO PORT:
app.listen(PORT, () => {
  console.log(`This application is listening on port ${PORT}`);
});

路由文件(userRouter.ts)

import express, { Router } from 'express';
import { loginUserController, logoutUserController, protectedRouteController, refreshTokenController, registerUserController } from '../controllers/userController';
import passport from 'passport';

const userRouter: Router = express.Router();

userRouter.post('/register', registerUserController);
userRouter.post('/login', loginUserController);
userRouter.get('/refreshToken', refreshTokenController);
userRouter.post('/logout', logoutUserController);

//protected route 
userRouter.get('/private',  passport.authenticate('oidc'),protectedRouteController);

// keycloak.protect()
export default userRouter;

问题定位

你的代码存在3个核心问题导致受保护路由失效:

  1. 策略初始化时机错误:Issuer.discover 是异步操作,Passport 策略在路由注册完成后才初始化,导致保护路由时策略未就绪
  2. 回调逻辑混乱:直接将受保护路由设为 Keycloak 的回调地址,违背 OpenID Connect 授权码流程的逻辑,无法完成认证闭环
  3. 中间件顺序错误:CORS、body-parser 等中间件放在 Passport 初始化之后,可能导致 Session 无法正确读取

分步修复指南

1. 调整中间件顺序

Express 中间件顺序决定执行优先级,正确顺序应为:

  • Session 中间件(必须在 Passport 之前)
  • Passport 初始化
  • CORS、body-parser、cookie-parser 等工具类中间件
  • 路由注册

2. 同步初始化 OpenID Client 策略

用 async/await 包裹 Issuer 发现逻辑,确保策略在路由注册前完成初始化,避免异步时序问题。

3. 拆分认证路由与受保护路由

专门设置回调路由处理 Keycloak 的授权响应,完成认证后再跳转到受保护页面;受保护路由使用 session 策略验证已登录状态。


正确配置模板

主文件(app.ts)

import express, { Express, Request, Response } from 'express';
import 'reflect-metadata';
import bodyparser from 'body-parser';
import userRouter from './api/routes/userRoutes';
import expressSession from 'express-session';
import cookieParser from 'cookie-parser';
import connection from './database/connection';
import cors from 'cors';
import { corsOptions } from './config/corsOptions';
import { credentials } from './api/middlewares/credentials';
import { Issuer, Strategy } from 'openid-client';
import passport from 'passport';
import config from './config';

const PORT = config.port || 3000;
const app: Express = express();

// 1. Session 中间件(必须前置)
const memoryStore = new expressSession.MemoryStore();
app.use(
  expressSession({
    secret: process.env.SESSION_SECRET || 'your-strong-secret-key',
    resave: false,
    saveUninitialized: false, // 避免创建空 Session
    store: memoryStore,
    cookie: {
      secure: process.env.NODE_ENV === 'production', // 生产环境启用 HTTPS
      httpOnly: true,
      maxAge: 24 * 60 * 60 * 1000 // Session 有效期1天
    }
  })
);

// 2. Passport 初始化
app.use(passport.initialize());
app.use(passport.session()); // 标准 Session 认证中间件

// 3. 工具类中间件
app.use(credentials);
app.use(cors(corsOptions));
app.use(express.json());
app.use(bodyparser.urlencoded({ extended: true }));
app.use(cookieParser());

// 4. 同步初始化 Keycloak OIDC 策略
(async () => {
  try {
    // 发现 Keycloak OIDC 配置
    const oidcIssuer = await Issuer.discover('http://localhost:8080/realms/Demo');
    
    // 创建 OIDC 客户端
    const client = new oidcIssuer.Client({
      client_id: 'keycloak_practice',
      client_secret: process.env.KEYCLOAK_CLIENT_SECRET || 'hkvogjo7jziNV3X4hR2rUILQikNKHpSL',
      redirect_uris: ['http://localhost:8000/user/auth/callback'], // 专门的回调地址
      post_logout_redirect_uris: ['http://localhost:8000/user/logout/callback'], // 登出回调
      response_types: ['code'],
    });

    // 配置 Passport OIDC 策略
    passport.use('oidc', new Strategy({ client }, (tokenSet, userinfo, done) => {
      // 可在此处将用户信息存入数据库,返回自定义用户对象
      return done(null, { ...tokenSet.claims(), userinfo });
    }));

    // 序列化/反序列化用户(生产环境建议仅存储用户ID)
    passport.serializeUser((user, done) => {
      done(null, user);
    });

    passport.deserializeUser((user, done) => {
      done(null, user);
    });

    // 5. 连接数据库
    await connection();

    // 6. 注册路由
    app.use('/user', userRouter);

    // 7. 启动服务
    app.listen(PORT, () => {
      console.log(`服务监听端口 ${PORT}`);
    });
  } catch (error) {
    console.error('Keycloak 客户端初始化失败:', error);
    process.exit(1);
  }
})();

路由文件(userRouter.ts)

import express, { Router } from 'express';
import { loginUserController, logoutUserController, protectedRouteController, refreshTokenController, registerUserController } from '../controllers/userController';
import passport from 'passport';

const userRouter: Router = express.Router();

// 本地注册/登录接口(若保留)
userRouter.post('/register', registerUserController);
userRouter.post('/login', loginUserController);
userRouter.get('/refreshToken', refreshTokenController);

// Keycloak 认证路由
// 触发 Keycloak 登录流程
userRouter.get('/auth/login', passport.authenticate('oidc'));

// Keycloak 回调路由:处理授权响应
userRouter.get('/auth/callback', 
  passport.authenticate('oidc', { failureRedirect: '/user/login' }),
  (req, res) => {
    // 认证成功后跳转到受保护页面
    res.redirect('/user/private');
  }
);

// 受保护路由:验证已登录状态
userRouter.get('/private', 
  passport.authenticate('session'), 
  protectedRouteController
);

// Keycloak 登出路由
userRouter.get('/logout', (req, res, next) => {
  req.logout((err) => {
    if (err) return next(err);
    // 跳转到 Keycloak 全局登出页面,完成后返回应用
    const logoutUrl = `${(req.user as any).issuer}/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent('http://localhost:8000/user/logout/callback')}`;
    res.redirect(logoutUrl);
  });
});

// 登出回调:清除 Session 并跳转首页
userRouter.get('/logout/callback', (req, res) => {
  res.clearCookie('connect.sid');
  res.redirect('/');
});

export default userRouter;

额外注意事项

  • 敏感信息管理:生产环境下,client_secret、session secret 必须从环境变量读取,禁止硬编码
  • Session 存储:MemoryStore 仅用于开发,生产环境需使用 Redis、MongoDB 等持久化存储
  • Keycloak 配置校验:确保 Keycloak 客户端的「Valid Redirect URIs」「Post Logout Redirect URIs」与代码配置完全一致
  • HTTPS 启用:生产环境必须启用 HTTPS,否则 Cookie 的 secure 属性无法生效,存在 Session 劫持风险

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 09:36:10