Ktor Server 中的 Digest 摘要认证
所需依赖:io.ktor:ktor-server-auth
代码示例: auth-digest
Digest 摘要认证方案是用于访问控制和身份验证的 HTTP 框架 的一部分。在这种方案中,哈希函数会在用户名和密码通过网络发送之前对其进行处理。
Ktor 支持 RFC 7616(HTTP Digest 摘要访问认证),它通过现代安全功能增强了旧的 RFC 2617,包括更强大的哈希算法、保护质量 (QoP) 选项以及用于保护隐私的用户名哈希。
Ktor 允许你使用 Digest 摘要认证来登录用户并保护特定的 路由。你可以在 Ktor Server 中的身份验证与授权 章节中获取有关 Ktor 身份验证的通用信息。
Digest 摘要认证提供比 基本认证 更强的安全性,因为密码永远不会以明文形式发送。然而,建议在生产环境中使用 HTTPS/TLS 以增加传输层安全性。
添加依赖
要启用 digest 身份验证,你需要在构建脚本中包含 ktor-server-auth 构件:
Digest 摘要认证流程
Digest 摘要认证流程如下:
客户端向服务器应用程序中的特定 路由 发起不带
Authorization标头的请求。服务器向客户端返回
401(Unauthorized) 响应状态,并使用WWW-Authenticate响应标头提供信息,说明该路由受 Digest 摘要认证方案保护。一个典型的WWW-Authenticate标头如下所示:WWW-Authenticate: Digest realm="Access to the '/' path", nonce="e4549c0548886bc2", algorithm=SHA-512-256, qop="auth"在 Ktor 中,你可以在 配置
digest身份验证提供程序时指定 realm、支持的算法、保护质量以及生成 nonce 值的方式。通常客户端会显示一个登录对话框,用户可以在其中输入凭据。然后,客户端会发起带有以下
Authorization标头的请求:Authorization: Digest username="jetbrains", realm="Access to the '/' path", nonce="e4549c0548886bc2", uri="/", algorithm=SHA-512-256, qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1"response值按以下方式生成:HA1 = H(username:realm:password),其中H是配置的哈希算法(例如 SHA-512-256)
这部分 存储 在服务器上,可供 Ktor 用于验证用户凭据。
HA2 = H(method:digestURI)(适用于qop=auth)或HA2 = H(method:digestURI:H(entityBody))(适用于qop=auth-int)response = H(HA1:nonce:nc:cnonce:qop:HA2)
服务器 验证 客户端发送的凭据并返回请求的内容。在使用 QoP 成功通过身份验证后,服务器还会返回
Authentication-Info标头以进行双向认证。
安装 Digest 摘要认证
要安装 digest 身份验证提供程序,请在 install 块内调用 digest 函数:
import io.ktor.server.application.*
import io.ktor.server.auth.*
// ...
install(Authentication) {
digest {
// 配置 digest 身份验证
}
}你可以选择性地指定一个 提供程序名称,该名称可用于 验证指定的路由。
配置 Digest 摘要认证
要了解如何在 Ktor 中配置不同身份验证提供程序的通用概念,请参阅 配置身份验证。在本节中,我们将了解 digest 身份验证提供程序的特定配置。
步骤 1:选择哈希算法
Ktor 为 Digest 摘要认证支持多种哈希算法。你可以使用 algorithms 属性配置服务器接受的算法:
| 算法 | 常量 | 安全级别 | 备注 |
|---|---|---|---|
| SHA-512-256 | DigestAlgorithm.SHA_512_256 | 推荐 | 安全性最强,建议用于新实现 |
| SHA-512-256-sess | DigestAlgorithm.SHA_512_256_SESS | 推荐 | 会话变体 - 在 HA1 中包含客户端 nonce |
| SHA-256 | DigestAlgorithm.SHA_256 | 良好 | 生产环境最低推荐 |
| SHA-256-sess | DigestAlgorithm.SHA_256_SESS | 良好 | 会话变体 - 在 HA1 中包含客户端 nonce |
| MD5 | DigestAlgorithm.MD5 | 已弃用 | 仅用于向后兼容 |
| MD5-sess | DigestAlgorithm.MD5_SESS | 已弃用 | 会话变体 - 仅用于旧版兼容 |
install(Authentication) {
digest("auth-digest") {
realm = "Access to the '/' path"
algorithms = listOf(DigestAlgorithm.SHA_512_256, DigestAlgorithm.MD5)
// ...
}
}当配置了多个算法时,服务器会发送多个 WWW-Authenticate 标头,允许客户端选择它们支持的最强算法。
默认算法为
SHA-512-256和MD5(用于与旧版本客户端的向后兼容)。
会话算法(-sess 变体)
-sess 算法变体(例如 SHA-512-256-sess、SHA-256-sess、MD5-sess)修改了 HA1 哈希的计算方式。会话算法计算 H(H(username:realm:password):nonce:cnonce),而不是存储 H(username:realm:password),其中 cnonce 是客户端提供的 nonce。
优点:
- 会话特定的哈希可防止预计算字典攻击
- 某个会话的哈希泄露不会暴露密码,也不会对其他会话造成帮助
缺点:
- 服务器必须为每个身份验证请求计算哈希(不能使用预计算的值)
对于大多数应用程序,标准(非会话)算法已经足够,尤其是与 SHA-512-256 等强哈希函数配合使用时。
步骤 2:提供带有摘要的用户表
digest 身份验证提供程序使用摘要消息的 HA1 部分来验证用户凭据,因此你可以提供一个包含用户名及其对应 HA1 哈希值的用户表。
由于不同的算法会产生不同的哈希值,你需要为你支持的每种算法存储适当的哈希值,或者根据客户端请求的算法动态计算哈希值:
val userPasswords: Map<String, String> = mapOf(
"jetbrains" to "foobar",
"admin" to "password"
)
fun computeHash(userName: String, realm: String, password: String, algorithm: DigestAlgorithm): ByteArray =
algorithm.toDigester().digest("$userName:$realm:$password".toByteArray(UTF_8))步骤 3:配置 Digest 提供程序
digest 身份验证提供程序通过 DigestAuthenticationProvider.Config 类公开其设置。在下面的示例中,指定了以下设置:
realm属性设置要在WWW-Authenticate标头中传递的 realm。algorithms属性指定要接受的哈希算法。digestProvider函数获取指定用户名和算法的摘要HA1部分。- (可选)
validate函数允许你将凭据映射到自定义 principal。
fun Application.main() {
install(Authentication) {
digest("auth-digest") {
realm = myRealm
// 支持现代 SHA-512-256 和旧版 MD5 客户端
algorithms = listOf(DigestAlgorithm.SHA_512_256, DigestAlgorithm.MD5)
digestProvider { userName, realm, algorithm ->
// 使用请求的算法计算 H(username:realm:password)
userPasswords[userName]?.let { password ->
computeHash(userName, realm, password, algorithm)
}
}
validate { credentials ->
if (credentials.userName.isNotEmpty()) {
CustomPrincipal(credentials.userName, credentials.realm)
} else {
null
}
}
}
}
}
data class CustomPrincipal(val userName: String, val realm: String)digestProvider 函数接收三个参数:
userName- 客户端请求中的用户名realm- 配置的 realmalgorithm- 客户端正在使用的哈希算法
你应该返回使用指定算法计算的 HA1 哈希,如果未找到用户,则返回 null。
你还可以使用 nonceManager 属性来指定如何生成 nonce 值。
步骤 4:配置保护质量
保护质量 (QoP) 决定了摘要计算中包含的内容:
DigestQop.AUTH- 仅身份验证(默认)。摘要包括请求方法和 URI。DigestQop.AUTH_INT- 带有完整性保护的身份验证。摘要还包括请求正文,提供防止篡改的保护。
install(Authentication) {
digest("auth-digest") {
realm = "Secure API"
supportedQop = listOf(DigestQop.AUTH, DigestQop.AUTH_INT)
// ...
}
}使用
auth-int时,请求正文会在身份验证期间被消耗。如果你需要在路由处理程序中访问正文,请安装 DoubleReceive 插件。
步骤 5:保护特定资源
配置好 digest 提供程序后,你可以使用 authenticate 函数保护应用程序中的特定资源。在身份验证成功的情况下,你可以在路由处理程序中使用 call.principal 函数检索经过身份验证的 Principal,并获取经过身份验证的用户名。
authenticate("auth-digest") {
get("/") {
call.respondText("Hello, ${call.principal<CustomPrincipal>()?.userName}!")
}
}
}
}
data class CustomPrincipal(val userName: String, val realm: String)高级配置
用户哈希支持
RFC 7616 引入了用户名哈希 (userhash) 以保护隐私。启用后,客户端可以发送用户名的哈希版本,而不是明文用户名。
要支持用户名哈希,请配置 userHashResolver:
val users = listOf("alice", "bob", "charlie")
install(Authentication) {
digest("auth-digest") {
realm = "Private API"
userHashResolver { userhash, realm, algorithm ->
// 从哈希中查找实际用户名
users.find { username ->
val digester = algorithm.toDigester()
val computedHash = hex(digester.digest("$username:$realm".toByteArray()))
computedHash == userhash
}
}
digestProvider { userName, realm, algorithm ->
// ...
}
}
}配置 userHashResolver 后,服务器会在 WWW-Authenticate 质询标头中宣告 userhash=true。
严格 RFC 7616 模式
为了在没有旧版客户端要求的新应用程序中获得最高安全性,请使用 strictRfc7616Mode():
install(Authentication) {
digest("auth-digest") {
realm = "Secure Zone"
strictRfc7616Mode()
digestProvider { userName, realm, algorithm ->
// 在严格模式下,算法永远不会是 MD5
}
}
}严格模式:
- 移除了 MD5 算法(仅允许 SHA-256、SHA-512-256 及其会话变体)
- 强制使用 UTF-8 字符集
UTF-8 字符集支持
digest 身份验证提供程序为包含非 ASCII 字符的用户名和密码支持 UTF-8 字符集:
install(Authentication) {
digest("auth-digest") {
realm = "My App"
charset = Charsets.UTF_8 // 这是默认值
// ...
}
}Authentication-Info 标头
在使用 QoP 成功通过身份验证后,服务器会自动返回 Authentication-Info 标头,其中包含:
rspauth- 用于双向认证的响应认证值nextnonce- 供客户端使用的下一个 nonceqop、nc、cnonce- 身份验证参数的回显
这允许客户端验证服务器的身份(双向认证)。
安全建议
使用 SHA-512-256 或 SHA-256 - 避免在生产环境中使用 MD5;它仅出于向后兼容性而包含。
使用
strictRfc7616Mode()- 适用于没有旧版客户端要求的新应用程序。实现适当的 nonce 管理 – 使用自定义
NonceManager以防止分布式环境中的重放攻击。考虑
auth-int- 当请求正文完整性对你的应用程序很重要时。启用
userhash- 以保护用户名的隐私。始终使用 HTTPS – 仅靠 Digest 摘要认证并不能加密流量;在生产环境中务必使用 TLS。
