解决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个核心问题导致受保护路由失效:
- 策略初始化时机错误:
Issuer.discover是异步操作,Passport 策略在路由注册完成后才初始化,导致保护路由时策略未就绪 - 回调逻辑混乱:直接将受保护路由设为 Keycloak 的回调地址,违背 OpenID Connect 授权码流程的逻辑,无法完成认证闭环
- 中间件顺序错误: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
相关产品推荐
相关产品推荐

