9、中间件与拦截器:自定义插件、请求日志、异常处理、CORS配置、速率限制

说到Ktor的中间件,我脑子里第一个蹦出来的词就是「管道」。Ktor整个请求处理流程,说白了就是一条管道。请求进来,经过一层层处理,最后到达你的业务代码。每一层都可以做点事情——记录日志、检查权限、转换数据、甚至直接掐断请求。

我个人习惯把中间件叫做「插件」,因为Ktor里就是这么命名的。你写一个类实现ApplicationPlugin,或者直接用install方法挂载官方提供的插件。今天我们就来聊聊怎么自己写插件,以及几个最常用的中间件场景。

9.1 自定义插件:从零开始写一个中间件

先看一个最简单的自定义插件。假设我想在每个请求处理前后打印一条日志:

class MyLoggingPlugin : ApplicationPlugin<Unit> {
    override val key = AttributeKey<MyLoggingPlugin>("MyLoggingPlugin")

    override fun install(pipeline: Application, configure: Unit.() -> Unit) {
        pipeline.intercept(ApplicationCallPipeline.Monitoring) {
            println("请求进来了: ${call.request.uri}")
            try {
                proceed()
            } finally {
                println("请求处理完毕: ${call.request.uri}")
            }
        }
    }
}

fun Application.myLogging() {
    install(MyLoggingPlugin)
}

这里要注意ApplicationCallPipeline.Monitoring这个阶段。Ktor把请求处理分成了几个阶段:SetupMonitoringCallFallback。我一般把日志、统计这类「只观察不干预」的逻辑放在Monitoring阶段。如果你要修改请求或响应,那就得用Call阶段。

小技巧: 自定义插件时,AttributeKey的泛型参数可以是你自己定义的配置类。这样就能像官方插件一样,在install时传入配置参数。

9.2 请求日志:别用println,用Logger

刚才的例子用了println,生产环境可不能这么干。Ktor自带了CallLogging插件,配置一下就能用:

install(CallLogging) {
    level = Level.INFO
    filter { call ->
        call.request.uri.startsWith("/api")
    }
    format { call ->
        "${call.request.httpMethod} ${call.request.uri} - ${call.response.status()}"
    }
}

我在项目中遇到过一个问题:日志里全是静态资源的请求,把真正的API日志淹没了。所以加了个filter,只记录/api开头的请求。另外,format可以自定义输出格式,我习惯把响应状态码也带上,方便排查问题。

注意: 不要在生产环境记录请求体(body),尤其是包含密码或Token的请求。如果确实需要,记得先脱敏。

9.3 异常处理:统一返回错误格式

我曾经接手过一个项目,每个接口的错误格式都不一样。有的返回{ "error": "xxx" },有的返回{ "message": "xxx" },前端对接的时候苦不堪言。后来我用StatusPages插件统一处理:

install(StatusPages) {
    exception<Throwable> { call, cause ->
        val status = when (cause) {
            is BadRequestException -> HttpStatusCode.BadRequest
            is NotFoundException -> HttpStatusCode.NotFound
            else -> HttpStatusCode.InternalServerError
        }
        call.respond(status, mapOf(
            "code" to status.value,
            "message" to (cause.message ?: "未知错误")
        ))
    }
}

这里有个坑:exception<Throwable>会捕获所有异常,包括KotlinNullPointerException这种。我建议你按具体异常类型分开处理,比如先处理BadRequestException,再处理NotFoundException,最后兜底用Throwable。顺序很重要,Ktor会匹配第一个符合条件的处理器。

9.4 CORS配置:别让前端骂娘

CORS(跨域资源共享)这个问题,说白了就是浏览器的一种安全机制。你的前端跑在localhost:3000,后端跑在localhost:8080,浏览器就会拦截跨域请求。Ktor的CORS插件配置起来很简单:

install(CORS) {
    allowHost("localhost:3000")
    allowHost("myapp.com", schemes = listOf("https"))
    allowMethod(HttpMethod.Options)
    allowHeader(HttpHeaders.ContentType)
    allowHeader(HttpHeaders.Authorization)
    allowCredentials = true
    maxAge = Duration.ofHours(1)
}

嗯,这里要注意几点:

  • allowCredentials = true 表示允许携带Cookie。如果你用了这个,allowHost就不能用通配符*,必须明确指定域名。
  • maxAge 是预检请求(OPTIONS)的缓存时间。设长一点能减少网络开销,我一般设1小时。
  • 如果你用了自定义Header,记得在allowHeader里加上,否则前端会报错。
避坑指南: 我曾经把allowHost写成了allowHost("*.myapp.com"),结果通配符只支持一级,sub.sub.myapp.com就不行。后来改成了allowHost("myapp.com", subDomains = listOf("sub"))才解决。

9.5 速率限制:保护你的API

速率限制(Rate Limiting)是防止API被滥用的重要手段。Ktor官方没有内置这个插件,但我们可以自己写一个简单的实现。思路是用一个Map记录每个IP的请求次数和时间:

class RateLimiter(
    private val maxRequests: Int = 100,
    private val windowMs: Long = 60_000
) {
    private val requests = mutableMapOf<String, MutableList<Long>>()

    fun isAllowed(ip: String): Boolean {
        val now = System.currentTimeMillis()
        val timestamps = requests.getOrPut(ip) { mutableListOf() }

        // 移除窗口外的记录
        timestamps.removeAll { it < now - windowMs }

        return if (timestamps.size < maxRequests) {
            timestamps.add(now)
            true
        } else {
            false
        }
    }
}

然后在插件里调用:

install(RateLimitingPlugin) {
    maxRequests = 100
    windowMs = 60_000
    // 超过限制时返回429
    onExceed = { call ->
        call.respond(HttpStatusCode.TooManyRequests, "请求过于频繁,请稍后再试")
    }
}
注意: 这个实现是单机版的,如果部署了多个实例,每个实例的计数器是独立的。生产环境建议用Redis实现分布式限流。另外,别忘了在响应头里加上X-RateLimit-Remaining,让客户端知道还剩多少次。

9.6 知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个速查表:

Ktor 中间件与拦截器知识体系 Application Plugin 自定义插件 请求日志 异常处理 CORS配置 速率限制 ApplicationPlugin接口 intercept + proceed() CallLogging插件 filter + format StatusPages插件 exception + respond allowHost + allowMethod allowCredentials 滑动窗口算法 429 Too Many Requests 核心原则: 插件化设计 · 关注点分离 · 统一错误处理 · 安全防护

9.7 总结

中间件和拦截器是Ktor框架的灵魂。你想想看,如果没有它们,每个接口都要自己写日志、自己处理异常、自己判断跨域,那代码得多臃肿。Ktor的插件机制让我们可以把这些横切关注点(cross-cutting concerns)集中管理,代码既干净又容易维护。

我个人建议,在项目初期就把这些基础插件搭好。别等到上线了才发现日志没配、CORS报错、异常信息泄露了敏感数据。嗯,这些都是我踩过的坑,希望你不用再踩一遍。

一句话总结: 插件是Ktor的「切面」,用好它们,你的后端API会既健壮又优雅。

公众号:蓝海资料掘金营,微信deep3321