SASL 把认证机制从协议里抽出来。这篇不连任何外部服务,直接让 JDK 自带的客户端与服务端互相认证,看有哪些机制、报文怎么走、协商出的保护层是什么,以及 unwrap 在遇到篡改时的真实行为。
[mechs]
客户端工厂:[ClientFactoryImpl[EXTERNAL, CRAM-MD5, PLAIN], FactoryImpl[GSSAPI], FactoryImpl[NTLM], ClientFactoryImpl[EXTERNAL, CRAM-MD5, PLAIN], FactoryImpl[DIGEST-MD5], ClientFactoryImpl[EXTERNAL, CRAM-MD5, PLAIN]]
服务端工厂:[FactoryImpl[DIGEST-MD5], FactoryImpl[NTLM], ServerFactoryImpl[CRAM-MD5], FactoryImpl[GSSAPI]]
SunSASL SaslClientFactory DIGEST-MD5
SunSASL SaslServerFactory DIGEST-MD5
SunSASL SaslClientFactory EXTERNAL
SunSASL SaslClientFactory CRAM-MD5
SunSASL SaslServerFactory CRAM-MD5
SunSASL SaslClientFactory PLAIN
SunSASL SaslServerFactory NTLM
SunSASL SaslClientFactory NTLM
JdkSASL SaslClientFactory GSSAPI
JdkSASL SaslServerFactory GSSAPI
Sasl.createSaslClient("PLAIN" ) = 有;createSaslServer = null(没有)
Sasl.createSaslClient("SCRAM-SHA-256") = null(没有);createSaslServer = null(没有)
Sasl.createSaslClient("OAUTHBEARER" ) = null(没有);createSaslServer = null(没有)
Sasl.createSaslClient("ANONYMOUS" ) = null(没有);createSaslServer = null(没有)
Sasl.createSaslClient("LOGIN" ) = null(没有);createSaslServer = null(没有)
mechanism 数组里有多个时按顺序选第一个能用的:PLAIN
机制列表 [PLAIN, CRAM-MD5]:不加属性选中 PLAIN;加 Sasl.POLICY_NOPLAINTEXT=true 选中 CRAM-MD5;加 POLICY_NOANONYMOUS / POLICY_NOACTIVE / POLICY_NODICTIONARY=true 对 CRAM-MD5 与 DIGEST-MD5 的影响:NODICTIONARY:CRAM-MD5 → null;DIGEST-MD5 → null [cram]
CRAM-MD5 好口令:成功;客户端 isComplete=true,服务端 isComplete=true;授权 ID amy;服务端回调 [NameCallback, PasswordCallback, AuthorizeCallback];交换了 3 个报文
客户端先发初始响应? false
服务端第 1 步 → <-6192225565412829630.1791056115839@localhost>(46B);isComplete=false
客户端 → amy 897d4b39b641c033227ddb06a0785bc3(36B);c.isComplete=true
服务端 → (null);s.isComplete=true
协商出的属性:QOP=auth,STRENGTH=null,MAX_BUFFER=null,RAW_SEND_SIZE=null
CRAM-MD5 错口令:失败 SaslException: Invalid response;交换了 2 个报文
客户端先发初始响应? false
服务端第 1 步 → <-2697646539720517548.1791056115851@localhost>(46B);isComplete=false
客户端 → amy 0888e6f48c3401854aebb065004553ba(36B);c.isComplete=true
CRAM-MD5 的 hasInitialResponse = false;getMechanismName = CRAM-MD5
同样的挑战、同样的口令,两次响应是否相同(确定性 HMAC-MD5,没有客户端随机数):true;响应 = amy ecc5ee0f37841e7fa4fd8acdff50eac0(36B)
手算 HMAC-MD5(口令, 挑战) 得到的响应:amy ecc5ee0f37841e7fa4fd8acdff50eac0;与客户端一致? true
已完成的客户端再 evaluateChallenge:IllegalStateException: CRAM-MD5 authentication already completed
服务端生成的挑战:<1656850057871703639.1791056115861@localhost>(45B)
响应的最后一个字节被改动:SaslException: Invalid response
用上一个服务端会话的挑战算出的响应去验证新会话(重放):SaslException: Invalid response [digest]
DIGEST-MD5 好口令(默认 QOP):成功;客户端 isComplete=true,服务端 isComplete=true;授权 ID amy;服务端回调 [RealmCallback, NameCallback, PasswordCallback, AuthorizeCallback];交换了 4 个报文
客户端先发初始响应? false
服务端第 1 步 → realm="localhost",nonce="EY4BrUzSJ3T94LYcp8bhgO5OOABB7Ucr1H1tlCbW",charset=utf-8,algorithm=md5-sess(99B);isComplete=false
客户端 → charset=utf-8,username="amy",realm="localhost",nonce="EY4BrUzSJ3T94LYcp8bhgO5OOABB7Ucr1H1tlCbW",nc=00000001,cnonce="CZx1xhxcOiq94HjrI913ORFX2bhgTzygJI…(249B);c.isComplete=false
服务端 → rspauth=548261afff7a988660ea466304a4fb6b(40B);s.isComplete=true
客户端 → (null);c.isComplete=true
协商出的属性:QOP=auth,STRENGTH=null,MAX_BUFFER=65536,RAW_SEND_SIZE=0
DIGEST-MD5 错口令:失败 SaslException: DIGEST-MD5: digest response format violation. Mismatched response.;交换了 2 个报文
客户端先发初始响应? false
服务端第 1 步 → realm="localhost",nonce="lhZQy18BdIhsTqmdtihxVBZ0ngNJvzGMNCZxk1Om",charset=utf-8,algorithm=md5-sess(99B);isComplete=false
客户端 → charset=utf-8,username="amy",realm="localhost",nonce="lhZQy18BdIhsTqmdtihxVBZ0ngNJvzGMNCZxk1Om",nc=00000001,cnonce="V0+cii9q4ybQ5Mxwu//hwpahFrCy3ZAA+g…(249B);c.isComplete=false [qop]
QOP 请求 “auth”:成功;客户端 isComplete=true,服务端 isComplete=true;授权 ID amy;服务端回调 [RealmCallback, NameCallback, PasswordCallback, AuthorizeCallback];交换了 4 个报文
协商出的属性:QOP=auth,STRENGTH=null,MAX_BUFFER=65536,RAW_SEND_SIZE=0
QOP 请求 “auth-int”:成功;客户端 isComplete=true,服务端 isComplete=true;授权 ID amy;服务端回调 [RealmCallback, NameCallback, PasswordCallback, AuthorizeCallback];交换了 4 个报文
协商出的属性:QOP=auth-int,STRENGTH=null,MAX_BUFFER=65536,RAW_SEND_SIZE=65520
QOP 请求 “auth-conf”:成功;客户端 isComplete=true,服务端 isComplete=true;授权 ID amy;服务端回调 [RealmCallback, NameCallback, PasswordCallback, AuthorizeCallback];交换了 4 个报文
协商出的属性:QOP=auth-conf,STRENGTH=high,MAX_BUFFER=65536,RAW_SEND_SIZE=65510
QOP 请求 “auth-conf,auth-int,auth”:成功;客户端 isComplete=true,服务端 isComplete=true;授权 ID amy;服务端回调 [RealmCallback, NameCallback, PasswordCallback, AuthorizeCallback];交换了 4 个报文
协商出的属性:QOP=auth-conf,STRENGTH=high,MAX_BUFFER=65536,RAW_SEND_SIZE=65510
客户端要 auth-conf、服务端只允许 auth:失败 SaslException: DIGEST-MD5: No common protection layer between client and server;交换了 1 个报文
auth-conf + STRENGTH=high:成功;客户端 isComplete=true,服务端 isComplete=true;授权 ID amy;服务端回调 [RealmCallback, NameCallback, PasswordCallback, AuthorizeCallback];交换了 4 个报文
协商出的属性:QOP=auth-conf,STRENGTH=high,MAX_BUFFER=65536,RAW_SEND_SIZE=65510
auth-conf + STRENGTH=low:成功;客户端 isComplete=true,服务端 isComplete=true;授权 ID amy;服务端回调 [RealmCallback, NameCallback, PasswordCallback, AuthorizeCallback];交换了 4 个报文
协商出的属性:QOP=auth-conf,STRENGTH=low,MAX_BUFFER=65536,RAW_SEND_SIZE=65510 [wrap]
[auth-int] 明文 39 字节 → wrap 后 55 字节(多 16 字节);包含明文? true;前 12 字节 61747461636b206174206461…(55B)
服务端 unwrap:attack at dawn, payload payload payload
改动 wrap 后字节中间一位再 unwrap:没有抛异常,返回 byte[0]
同一条消息第二次 wrap 的密文是否与第一次相同:false
把第一条 wrap 的字节再 unwrap 一次(重放,序号已前进):SaslException: DIGEST-MD5: Out of order sequencing of messages from server. Got: 0 Expected: 1
先 unwrap 第二条:attack at dawn, payload payload payload
CRAM-MD5(没有 QOP 协商)上调用 wrap:IllegalStateException: CRAM-MD5 authentication not completed
[auth-conf] 明文 39 字节 → wrap 后 62 字节(多 23 字节);包含明文? false;前 12 字节 9351eb15118a6530dc6e052a…(62B)
服务端 unwrap:attack at dawn, payload payload payload
改动 wrap 后字节中间一位再 unwrap:没有抛异常,返回 byte[0]
同一条消息第二次 wrap 的密文是否与第一次相同:false
把第一条 wrap 的字节再 unwrap 一次(重放,序号已前进):没有抛异常,返回 byte[0]
先 unwrap 第二条:attack at dawn, payload payload payload
CRAM-MD5(没有 QOP 协商)上调用 wrap:IllegalStateException: CRAM-MD5 authentication not completed
Oct 04, 2026 3:35:17 AM com.sun.security.sasl.digest.DigestMD5Base$DigestIntegrity unwrap
INFO: DIGEST23:Unmatched MACs
Oct 04, 2026 3:35:17 AM com.sun.security.sasl.digest.DigestMD5Base$DigestPrivacy unwrap
INFO: DIGEST41:Unmatched MACs
Oct 04, 2026 3:35:17 AM com.sun.security.sasl.digest.DigestMD5Base$DigestPrivacy unwrap
INFO: DIGEST41:Unmatched MACs [client]
EXTERNAL 客户端:hasInitialResponse = true;初始响应 amy(3B)(授权 ID 直接作为响应,认证由底层(如 TLS 客户端证书)完成)
EXTERNAL 不带授权 ID:(0B)
createSaslClient 的 handler 为 null 而机制需要口令:SaslException: Callback handler to get username/password required
DIGEST-MD5 服务端没有 handler:SaslException: Callback handler with support for AuthorizeCallback, RealmCallback, NameCallback, and PasswordCallback required
协议名 / 服务器名为 null:成功 / 成功