使用 Ktor 来反向代理 Keycloak

本文介绍了如何使用Ktor框架构建一个反向代理,保护Keycloak服务器。作者详细描述了配置过程,包括依赖项选择、HttpClient的使用以及处理重定向等问题的解决方案。
摘要由CSDN通过智能技术生成

The English version of this article is available on Medium.

Ktor 是一个用于轻松构建异步服务器端和客户端应用程序的框架,使用 Kotlin 编写。它支持许多引擎(服务端/客户端),包括 Ktor 团队开发的 CIO(基于协程的 I/O)引擎。

今天我们要在 Ktor 上实现一个可用的反向代理,用来保护它背后的 Keycloak 服务器。鼓捣了半天才发现有示例,不过没关系,用这个示例代理 Keycloak 还是不太行的。

了解 Ktor 的部分我就不过多赘述了,直接开始敲代码。

依赖项

依赖项其实很简单,一个服务端引擎和一个客户端引擎即可,我们在这里使用 CIO。

# Gradle version catalog

[versions]
ktor = "3.0.0-beta-1"

[libraries]
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }

[plugins]
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }

主应用程序(Application)和模块(module)

主应用程序可以选择 embeddedServerEngineMain,可根据项目情况进行配置。

我们打算不在主模块中添加过多内容,而是把它们拆分到单独的文件中。简单的主模块:

fun Application.module() {
    routing {
        proxyKeycloak()
    }
}

反向代理

这回进入正题了。反向代理,说白了就是一台服务器接收到请求,转发到目标服务器,接收到目标服务器的响应后,再转发回客户端。所以我们的应用中需要先配置一个客户端。

客户端

// proxy/Keycloak.kt

private val keycloakProxyClient = HttpClient()

非常简洁的一个客户端。请注意,如果你向项目中添加了多个客户端引擎,需要显式指定。例子如下:

HttpClient(CIO)

Keycloak

这时我们指定两个主机名:ktor(端口 8080)和 keycloak(端口 8081)。

我们假定 Keycloak 托管在路径 /auth 下,需要对 Keycloak 进行如下配置红帽的中文版文档)。

http-enabled=true # 对进入 Keycloak 的请求启用 HTTP
http-port=8081
proxy=edge
http-relative-path=/auth
hostname-strict=false

代理

配置完毕,就可以开始编写 proxyKeycloak() 了。

// proxy/Keycloak.kt

fun Route.proxyKeycloak() {
    route(Regex("/auth|/auth/.*")) {
        // TODO
    }
}

我们使用 route DSL 指定了一个路径,告诉 Ktor 匹配这个正则表达式的请求交给这个 lambda 处理。Ktor 提供了 getpost 扩展函数,但是我们需要代理所有请求,故不使用它们。

接下来是处理请求和发送新请求。

// proxy/Keycloak.kt

@OptIn(InternalAPI::class)
fun Route.proxyKeycloak() {
    route(Regex("/auth|/auth/.*")) {
        handle {
            keycloakProxyClient.request {
                url("http://$keycloakHost:$keycloakPort${call.request.uri}")
                method = call.request.httpMethod
                headers.appendAll(call.request.headers) // 哈?
                // 嗯?
                body = call.receive()
            }.let { response ->
                call.respond(object : OutgoingContent.WriteChannelContent() {
                    override val contentLength: Long? = response.contentLength()
                    override val contentType: ContentType? = response.contentType()
                    override val status: HttpStatusCode = response.status
                    override val headers: Headers = Headers.build {
                        appendAll(response.headers) // 啊?
                    }

                    override suspend fun writeTo(channel: ByteWriteChannel) {
                        response.content.copyAndClose(channel)
                    }
                })
            }
        }
    }
}

到这里好像就大功告成了,但很显然是不可以的,因为我们还没有配置 Forwarded 标头(也可以用 X- 非标标头,不过既然标准的摆在这,就用了吧)。

标头

嗯? 处插入:

headers[HttpHeaders.Forwarded] = "for=${call.request.local.remoteHost};host=${call.request.headers[HttpHeaders.Host] ?: ""};proto=${call.request.local.scheme}"

这样我们就可以告诉 Keycloak,发出请求的真实地址是什么。

不过,现在我们还是不能让代理服务器变得可用,因为 Ktor 管理了一些标头,并且不允许手动指定,否则会引发异常,造成 500 错误。让我们排除这些标头。

val Headers.safeExplicitness get() = filter { key, _ -> 
    !key.equals(
        HttpHeaders.TransferEncoding, ignoreCase = true
    ) && !key.equals(
        HttpHeaders.ContentType, ignoreCase = true
    ) && !key.equals(
        HttpHeaders.ContentLength, ignoreCase = true
    )
}

啊? 处修改为:

appendAll(response.headers.safeExplicitness)

哈? 怎么办呢?如果对它也应用 safeExplicitness,会导致发送给 Keycloak 的信息不正确:

{"error":"invalid_request","error_description":"Missing form parameter: grant_type"}

打开浏览器的开发者工具会发现,grant_type 其实已经被指定了,于是乎,哈? 处只能修改为:

headers.appendAll(call.request.headers)
headers.remove(HttpHeaders.TransferEncoding)
headers[HttpHeaders.Host] = "$keycloakHost:$keycloakPort" // 还需要加上真实的 Host 标头

这回访问 Keycloak 不会报异常了。

不要重定向

这也是一个坑点,HttpClient 默认跟随重定向,这会导致发送给 Keycloak 的重定向 URI 出现循环(还记得我们前面提过的主机名吗,不记得的话,先回去看一下再浏览下面的日志):

WARN  [org.keycloak.events] (executor-thread-123) type="LOGIN_ERROR", ***, error="invalid_redirect_uri", redirect_uri="http://ktor/***/auth?***&redirect_uri=http%3A%2F%2Fktor%2F***%2F&state=***&response_mode=fragment&response_type=code&scope=openid&nonce=***&prompt=none&code_challenge=***&code_challenge_method=***"

发现了吗,它会把我们重定向到一个新的认证请求上。当然,应用默认策略不允许这样的重定向 URI,于是这样的请求会被屏蔽,导致无法登录。

解决方法也很简单:

private val keycloakProxyClient = HttpClient {
    followRedirects = false
}

这样,我们的反向代理的 Keycloak 就完全可用了。

保护一下

还要避免公开一些不该公开的东西,把路径的表达式替换为 /auth/((js|realms|resources)/.*|robots.txt|favicon.ico) 即可。

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值