NestJS集成Swagger UI对接Keycloak OAuth时CORS跨域报错问题
NestJS Swagger 接入Keycloak OAuth2鉴权CORS报错排查方案
问题背景
- 基于NestJS搭建的API服务已全量接入Keycloak鉴权,现有鉴权逻辑运行正常
- 需求为通过
@nestjs/swagger生成API文档,支持用户在Swagger UI端直接完成OAuth认证后调用接口,支持两种授权触发方式:打开Swagger UI时自动初始化登录、点击页面「Authorize」按钮手动授权 - 已按照NestJS官方OpenAPI模块文档配置,且开启了CORS(甚至尝试过指定origin为
https://localhost:3334等全量参数),仍持续出现CORS报错
现有main.ts配置代码
import { NestFactory } from '@nestjs/core' import { AppModule } from '@root/app.module' import { DBService } from '@middleware/db.service' import * as fs from 'fs' import * as path from 'path' import { Logger } from '@nestjs/common' import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' async function bootstrap() { const ssl = process.env.SSL === 'true' ? true : false let httpsOptions = null if (ssl) { const keyPath = process.env.SSL_KEY_PATH || '' const certPath = process.env.SSL_CERT_PATH || '' httpsOptions = { key: fs.readFileSync(path.join(__dirname, keyPath), 'utf8'), cert: fs.readFileSync(path.join(__dirname, certPath), 'utf8') } } const app = await NestFactory.create(AppModule, { httpsOptions }) app.enableCors() // swagger配置段 const config = new DocumentBuilder() .addOAuth2( { type: 'oauth2', flows: { password: { tokenUrl: `${process.env.KEYCLOAK_AUTH_URL}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, authorizationUrl: `${process.env.KEYCLOAK_AUTH_URL}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`, scopes: {} } } }) .setTitle('MyAPI') .setDescription('API description') .setVersion('0.1') .addTag('AM') .build() const document = SwaggerModule.createDocument(app, config) SwaggerModule.setup('api', app, document, { swaggerOptions: { oauth: { clientID: process.env.KEYCLOAK_CLIENT_ID, realm: process.env.KEYCLOAK_REALM, appName: 'swagger-ui' } } }) const port = Number(process.env.PORT) || 3333 const hostname = process.env.HOSTNAME || 'localhost' const dbService: DBService = app.get(DBService) dbService.enableShutdownHooks(app) await app.listen(port, hostname, () => { const address = 'http' + (ssl ? 's' : '') + '://' + hostname + ':' + port + '/' Logger.log('Listening at ' + address) }); } bootstrap();
问题解答
1. CORS报错根本原因
你在NestJS服务中配置的app.enableCors()仅作用于发往NestJS服务自身的请求,本次报错的跨域请求是Swagger UI前端直接向Keycloak服务发起的,完全不经过NestJS服务,因此无论怎么调整NestJS侧的CORS配置都无法解决问题。
触发报错的核心原因分三类:
- Keycloak侧客户端配置错误:用于Swagger对接的Keycloak客户端,未将Swagger UI的访问源、重定向地址加入白名单,Keycloak收到跨域请求时返回的响应不带合法CORS头,被浏览器拦截。
- 授权模式选择错误:当前配置使用
password密码模式,该模式不支持浏览器端跳转授权流程,Swagger UI会直接从前端发起跨域令牌请求,Keycloak默认拒绝该类跨域请求。 - 端点路径错误:17及以上版本的Keycloak(基于Quarkus发行)默认移除了接口路径的
/auth前缀,若你使用新版Keycloak却保留了/auth前缀,请求会直接返回404,404响应自然不会携带CORS头,同样会触发跨域报错。
2. 当前Swagger OAuth配置合规性判断
现有配置不符合对接规范,存在3个明确问题:
- 授权流选型错误:浏览器端Swagger UI对接Keycloak必须使用
authorizationCode授权码模式,password模式已被OAuth2.1标记为废弃,且不适合纯前端场景使用。 - OAuth初始化参数缺失:Swagger的oauth配置未开启PKCE校验(Keycloak公共客户端默认强制要求PKCE)、未携带必填的
openidscope(Keycloak基于OIDC协议,必须携带该scope才能完成认证),也未配置页面加载自动授权的相关参数。 - 未配置全局安全要求:仅添加了OAuth2安全定义,但未调用
.addSecurityRequirements('oauth2')将安全规则应用到所有接口,即使授权成功,调用接口时Swagger也不会自动携带令牌。
3. 正确配置步骤
按以下顺序调整即可实现需求:
- 调整Keycloak客户端配置
- 将客户端访问类型设置为
public(公共客户端,无需在前端暴露客户端密钥) - 「有效重定向URI」中添加Swagger的回调地址,格式为
你的Swagger访问域名/oauth2-redirect.html,例如https://localhost:3333/api/oauth2-redirect.html - 「Web源」配置中添加Swagger的访问源,例如
https://localhost:3333 - 客户端认证流仅开启「标准流」(即授权码流),关闭其余不需要的流
- 将客户端访问类型设置为
- 替换原有Swagger配置代码
// 请根据自身Keycloak版本调整路径前缀:17+版本去掉路径中的/auth段 const keycloakBase = process.env.KEYCLOAK_AUTH_URL const keycloakRealm = process.env.KEYCLOAK_REALM const keycloakClient = process.env.KEYCLOAK_CLIENT_ID const serviceBaseUrl = `http${ssl ? 's' : ''}://${hostname}:${port}` const config = new DocumentBuilder() .addOAuth2({ type: 'oauth2', flows: { authorizationCode: { authorizationUrl: `${keycloakBase}/realms/${keycloakRealm}/protocol/openid-connect/auth`, tokenUrl: `${keycloakBase}/realms/${keycloakRealm}/protocol/openid-connect/token`, scopes: { openid: 'OIDC基础权限' } } } }) .setTitle('MyAPI') .setDescription('API description') .setVersion('0.1') .addTag('AM') // 全局应用OAuth2鉴权规则 .addSecurityRequirements('oauth2') .build() const document = SwaggerModule.createDocument(app, config) SwaggerModule.setup('api', app, document, { swaggerOptions: { oauth: { clientId: keycloakClient, appName: 'swagger-ui', usePkceWithAuthorizationCodeGrant: true, scopes: ['openid'] }, persistAuthorization: true, oauth2RedirectUrl: `${serviceBaseUrl}/api/oauth2-redirect.html`, // 打开Swagger页面时自动触发授权登录 initOAuth: true } })
- 若本地部署使用自签名SSL证书,需提前在浏览器中信任Keycloak和NestJS服务的证书,证书校验失败也会被浏览器归类为跨域类报错。
内容的提问来源于stack exchange,提问作者memphisten
相关产品推荐
相关产品推荐

