使用 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
    评论
Ktor是一个开源的Kotlin框架,它可以用于构建异步和非阻塞的Web应用程序、服务和API。它具有轻量级、易于使用和快速开发的特点,因此被越来越多的开发者使用。 下面是使用Ktor进行服务开发的步骤: 1. 创建Ktor项目 首先,需要创建一个Ktor项目。可以使用IntelliJ IDEA或其他Kotlin IDE创建一个新项目,或者使用Ktor提供的命令行工具创建一个项目。运行以下命令: ``` $ mkdir myapp && cd myapp $ gradle init --type=kotlin-application ``` 这将创建一个基本的Ktor项目,其中包括一个Gradle构建文件和一个Kotlin文件。 2. 添加Ktor依赖 在build.gradle文件中添加Ktor依赖: ``` dependencies { implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "io.ktor:ktor-html-builder:$ktor_version" implementation "io.ktor:ktor-jackson:$ktor_version" implementation "io.ktor:ktor-client-okhttp:$ktor_version" testImplementation "io.ktor:ktor-server-tests:$ktor_version" } ``` 3. 创建路由 创建路由是使用Ktor创建服务的关键步骤之一。在Ktor中,路由是一个函数,用于处理HTTP请求。可以使用以下代码创建路由: ``` routing { get("/") { call.respondText("Hello, world!") } get("/users") { val users = listOf("Alice", "Bob", "Charlie") call.respond(users) } post("/users") { val user = call.receive<User>() // process user call.respond(HttpStatusCode.Created) } } ``` 在上面的示例中,路由函数处理三个不同的HTTP请求: - GET /:返回“Hello, world!”字符串。 - GET /users:返回一个名为“users”的列表。 - POST /users:接收一个名为“user”的请求体,并返回HTTP状态码201。 4. 运行服务 完成路由之后,可以运行服务并测试它。在命令行中运行以下命令: ``` $ ./gradlew run ``` 然后在浏览器中访问http://localhost:8080/,应该会看到“Hello, world!”的消息。 这就完成了使用Ktor进行服务开发的步骤。Ktor具有可扩展性,可以使用各种插件和中间件来增强功能,例如安全性、WebSockets和GraphQL支持等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值