Skip to content
Server Plugin

Ktor Server 中的认证与授权

所需依赖项io.ktor:ktor-server-auth

Ktor 提供了 Authentication 插件来处理认证与授权。典型使用场景包括用户登录、授予对特定资源的访问权限,以及在各方之间安全地传输信息。你还可以将 Authentication会话 结合使用,以在请求之间保留用户信息。

在客户端,Ktor 提供了 Authentication 插件来处理认证与授权。

支持的认证类型

Ktor 支持以下认证与授权方案:

HTTP 认证

HTTP 为访问控制和认证提供了一个 通用框架。在 Ktor 中,你可以使用以下 HTTP 认证方案:

  • Basic - 使用 Base64 编码提供用户名和密码。如果未结合 HTTPS 使用,通常不建议使用。
  • Digest - 一种认证方法,通过对用户名和密码应用哈希函数,以加密形式传输用户凭据。
  • Bearer - 一种涉及称为持有者令牌 (bearer tokens) 的安全令牌的认证方案。 Bearer 认证方案作为 OAuthJWT 的一部分使用,但你也可以为授权持有者令牌提供自定义逻辑。
  • API Key - 一种简单的认证方法,客户端在标头中传递密钥。

基于表单的认证

基于表单 的认证使用 Web 表单 来收集凭据信息并认证用户。

JSON Web 令牌 (JWT)

JSON Web Token 是一个开放标准,用于作为 JSON 对象在各方之间安全地传输信息。你可以将 JSON Web 令牌用于授权:当用户登录后,每个请求都将包含一个令牌,允许用户访问该令牌许可的资源。在 Ktor 中,你可以使用 jwt 认证来验证令牌并校验其中包含的声明 (claims)。

LDAP

LDAP 是一个用于目录服务认证的开放式跨平台协议。Ktor 提供了 ldapAuthenticate 函数,用于根据指定的 LDAP 服务器认证用户凭据。

OAuth

OAuth 是用于保护 API 访问的开放标准。Ktor 中的 oauth 提供者允许你使用外部提供者(如 Google、Facebook、Twitter 等)实现认证。

会话

会话 提供了一种在不同 HTTP 请求之间持久化数据的机制。典型用例包括存储已登录用户的 ID、购物车内容或在客户端保留用户偏好设置。在 Ktor 中,已经拥有关联会话的用户可以使用 session 提供者进行认证。详细了解如何从 Ktor Server 中的会话认证 执行此操作。

自定义

Ktor 提供了两种方式来自定义认证与授权行为:

添加依赖项

要使用 Authentication,你需要在构建脚本中包含 ktor-server-auth 构件:

Kotlin
Groovy
XML

注意,某些认证提供者(如 JWTLDAP)需要额外的构件。

安装 Authentication

要将 Authentication 插件安装到应用程序,请在指定的

模块
模块允许你通过对路由进行分组来构建应用程序。
中将其传递给 install 函数。 下面的代码片段展示了如何安装 Authentication ...

  • ... 在 embeddedServer 函数调用中。
  • ... 在显式定义的 module(它是 Application 类的扩展函数)中。
kotlin
kotlin

配置 Authentication

安装 Authentication 之后,你可以按照以下步骤配置并使用 Authentication

步骤 1:选择认证提供者

要使用特定的认证提供者,例如 basicdigestform,你需要在 install 块中调用相应的函数。例如,要使用 basic 认证,请调用 .basic() 函数:

kotlin
import io.ktor.server.application.*
import io.ktor.server.auth.*
// ...
install(Authentication) {
    basic {
        // 配置 basic 认证
    }
}

在此函数内部,你可以 配置 该提供者特有的设置。

如果内置提供者不符合你的要求,你可以实现 自定义认证提供者

步骤 2:指定提供者名称

用于 使用特定提供者 的函数可选地允许你指定提供者名称。下面的代码示例分别使用 "auth-basic""auth-form" 名称安装了 basicform 提供者:

kotlin
install(Authentication) {
    basic("auth-basic") {
        // 配置 basic 认证
    }
    form("auth-form") {
        // 配置 form 认证
    }
    // ...
}

这些名称稍后可用于使用不同的提供者来 认证不同的路由

注意,提供者名称应该是唯一的,并且你只能定义一个不带名称的提供者。

步骤 3:配置提供者

每种 提供者类型 都有其自己的配置。例如,BasicAuthenticationProvider.Config 类为 .basic() 函数提供选项。该类中的关键函数是 validate(),它负责验证用户名和密码。以下代码示例演示了其用法:

kotlin
install(Authentication) {
    basic("auth-basic") {
        realm = "Access to the '/' path"
        validate { credentials ->
            if (credentials.name == "jetbrains" && credentials.password == "foobar") {
                UserIdPrincipal(credentials.name)
            } else {
                null
            }
        }
    }
}

要理解 validate() 函数的工作原理,我们需要引入两个术语:

  • principal (主体) 是一个可以被认证的实体:用户、计算机、服务等。在 Ktor 中,各种认证提供者可能会使用不同的主体。例如,basicdigestform 提供者认证 UserIdPrincipal,而 jwt 提供者验证 JWTPrincipal

    你还可以创建自定义主体。这在以下情况下可能很有用:

    • 将凭据映射到自定义主体允许你在 路由处理程序 中拥有关于已认证主体的额外信息。
    • 如果你使用 会话认证,主体可能是一个存储会话数据的数据类。
  • credential (凭据) 是一组供服务器认证主体的属性:用户名/密码对、API 密钥等。例如,basicform 提供者使用 UserPasswordCredential 来验证用户名和密码,而 jwt 验证 JWTCredential

因此,validate() 函数检查指定的凭据,并在认证成功时返回一个主体 Any,如果认证失败则返回 null

要根据特定标准跳过认证,请使用 skipWhen()。例如,如果 会话 已存在,你可以跳过 basic 认证:

kotlin
basic {
    skipWhen { call -> call.sessions.get<UserSession>() != null }
}

步骤 4:保护特定资源

最后一步是保护应用程序中的特定资源。你可以通过使用 authenticate() 函数来实现。该函数接受两个可选参数:

  • 用于认证嵌套路由的 提供者名称。 下面的代码片段使用名为 auth-basic 的提供者来保护 /login/orders 路由:

    kotlin
    routing {
        authenticate("auth-basic") {
            get("/login") {
                // ...
            }    
            get("/orders") {
                // ...
            }    
        }
        get("/") {
            // ...
        }
    }
  • 用于解析嵌套认证提供者的策略。 该策略由 AuthenticationStrategy 枚举值表示。

    例如,客户端应为所有使用 AuthenticationStrategy.Required 策略注册的提供者提供认证数据。 在下面的代码片段中,只有通过了 会话认证 的用户才能尝试使用 basic 认证访问 /admin 路由:

    kotlin
    routing {
        authenticate("auth-session", strategy = AuthenticationStrategy.Required) {
            get("/hello") {
                // ...
            }    
            authenticate("auth-basic", strategy = AuthenticationStrategy.Required) {
                get("/admin") {
                    // ...
                }
            }  
        }
    }

有关完整示例,请参阅 auth-form-session-nested

步骤 5:在路由处理程序中获取主体

认证成功后,你可以使用 call.principal() 函数在路由处理程序中检索已认证的主体。该函数接受由 配置的认证提供者 返回的特定主体类型。在以下示例中,使用 call.principal() 获取 UserIdPrincipal 并获取已认证用户的名称。

kotlin
routing {
    authenticate("auth-basic") {
        get("/") {
            call.respondText("Hello, ${call.principal<UserIdPrincipal>()?.name}!")
        }
    }
}

如果你使用 会话认证,主体可能是一个存储会话数据的数据类。因此,你需要将此数据类传递给 call.principal()

kotlin
authenticate("auth-session") {
    get("/hello") {
        val userSession = call.principal<UserSession>()
    }
}

嵌套认证提供者 的情况下,你可以将 提供者名称 传递给 call.principal() 以获取所需提供者的主体。

在下面的示例中,传递 "auth-session" 值以获取最顶层会话提供者的主体:

kotlin
authenticate("auth-session", strategy = AuthenticationStrategy.Required) {
    authenticate("auth-basic", strategy = AuthenticationStrategy.Required) {
        get("/admin") {
            val userSession = call.principal<UserSession>("auth-session")
        }
    }
}

自定义认证提供者

当内置提供者不符合你的要求时,请使用 provider() 函数来实现自定义认证逻辑:

kotlin
provider("custom") {
  authenticate { context ->
    val exampleHeader = context.call.request.headers["Example-Header"]
    if (exampleHeader == null) {
      val cause = AuthenticationFailedCause.Error("No example header found")
      context.challenge(key = this, cause) { challenge, call ->
        call.respondText("Challenge")
        challenge.complete()
      }
    }
  }
}

在上述示例中,provider("custom") 函数注册了一个命名的认证提供者,稍后可以使用 authenticate("custom") 将其应用于路由。

在提供者内部,authenticate {} 块针对每个传入请求执行,并允许你通过上下文对象完全控制认证过程。这包括访问当前调用 (context.call) 以及检查标头、参数或其他请求数据的能力。

你可以使用 DynamicProviderConfig 类提供的选项来配置额外的提供者行为。