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 认证方案作为 OAuth 或 JWT 的一部分使用,但你也可以为授权持有者令牌提供自定义逻辑。
- 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 提供了两种方式来自定义认证与授权行为:
- 使用 自定义认证提供者。
- 使用 自定义插件 来实现授权逻辑。例如,你可以使用
AuthenticationChecked钩子 来验证访问权限。更多信息请参阅 custom-plugin-authorization 示例。
添加依赖项
要使用 Authentication,你需要在构建脚本中包含 ktor-server-auth 构件:
注意,某些认证提供者(如 JWT 和 LDAP)需要额外的构件。
安装 Authentication
要将 Authentication 插件安装到应用程序,请在指定的
install 函数。 下面的代码片段展示了如何安装 Authentication ... - ... 在
embeddedServer函数调用中。 - ... 在显式定义的
module(它是Application类的扩展函数)中。
配置 Authentication
在 安装 Authentication 之后,你可以按照以下步骤配置并使用 Authentication:
步骤 1:选择认证提供者
要使用特定的认证提供者,例如 basic、digest 或 form,你需要在 install 块中调用相应的函数。例如,要使用 basic 认证,请调用 .basic() 函数:
import io.ktor.server.application.*
import io.ktor.server.auth.*
// ...
install(Authentication) {
basic {
// 配置 basic 认证
}
}在此函数内部,你可以 配置 该提供者特有的设置。
如果内置提供者不符合你的要求,你可以实现 自定义认证提供者。
步骤 2:指定提供者名称
用于 使用特定提供者 的函数可选地允许你指定提供者名称。下面的代码示例分别使用 "auth-basic" 和 "auth-form" 名称安装了 basic 和 form 提供者:
install(Authentication) {
basic("auth-basic") {
// 配置 basic 认证
}
form("auth-form") {
// 配置 form 认证
}
// ...
}这些名称稍后可用于使用不同的提供者来 认证不同的路由。
注意,提供者名称应该是唯一的,并且你只能定义一个不带名称的提供者。
步骤 3:配置提供者
每种 提供者类型 都有其自己的配置。例如,BasicAuthenticationProvider.Config 类为 .basic() 函数提供选项。该类中的关键函数是 validate(),它负责验证用户名和密码。以下代码示例演示了其用法:
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 中,各种认证提供者可能会使用不同的主体。例如,
basic、digest和form提供者认证UserIdPrincipal,而jwt提供者验证JWTPrincipal。你还可以创建自定义主体。这在以下情况下可能很有用:
- credential (凭据) 是一组供服务器认证主体的属性:用户名/密码对、API 密钥等。例如,
basic和form提供者使用UserPasswordCredential来验证用户名和密码,而jwt验证JWTCredential。
因此,validate() 函数检查指定的凭据,并在认证成功时返回一个主体 Any,如果认证失败则返回 null。
要根据特定标准跳过认证,请使用
skipWhen()。例如,如果 会话 已存在,你可以跳过basic认证:kotlinbasic { skipWhen { call -> call.sessions.get<UserSession>() != null } }
步骤 4:保护特定资源
最后一步是保护应用程序中的特定资源。你可以通过使用 authenticate() 函数来实现。该函数接受两个可选参数:
用于认证嵌套路由的 提供者名称。 下面的代码片段使用名为 auth-basic 的提供者来保护
/login和/orders路由:kotlinrouting { authenticate("auth-basic") { get("/login") { // ... } get("/orders") { // ... } } get("/") { // ... } }用于解析嵌套认证提供者的策略。 该策略由
AuthenticationStrategy枚举值表示。例如,客户端应为所有使用
AuthenticationStrategy.Required策略注册的提供者提供认证数据。 在下面的代码片段中,只有通过了 会话认证 的用户才能尝试使用 basic 认证访问/admin路由:kotlinrouting { 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 并获取已认证用户的名称。
routing {
authenticate("auth-basic") {
get("/") {
call.respondText("Hello, ${call.principal<UserIdPrincipal>()?.name}!")
}
}
}如果你使用 会话认证,主体可能是一个存储会话数据的数据类。因此,你需要将此数据类传递给 call.principal():
authenticate("auth-session") {
get("/hello") {
val userSession = call.principal<UserSession>()
}
}在 嵌套认证提供者 的情况下,你可以将 提供者名称 传递给 call.principal() 以获取所需提供者的主体。
在下面的示例中,传递 "auth-session" 值以获取最顶层会话提供者的主体:
authenticate("auth-session", strategy = AuthenticationStrategy.Required) {
authenticate("auth-basic", strategy = AuthenticationStrategy.Required) {
get("/admin") {
val userSession = call.principal<UserSession>("auth-session")
}
}
}自定义认证提供者
当内置提供者不符合你的要求时,请使用 provider() 函数来实现自定义认证逻辑:
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 类提供的选项来配置额外的提供者行为。
