bigywater.tech
Java · 362

PasswordCallback:认证过程中如何请求密码

JAAS 把「向谁要口令、要什么」拆成 LoginModule 与 CallbackHandler。这篇用真实运行的模块,逐个看回调对象怎么保存口令、多个模块怎么各自提问、出错时是什么样子。

副本
PasswordCallback.setPassword / getPassword 都是副本,clearPassword 只清内部那一份
字节流里有口令
序列化一个设置了口令的 PasswordCallback 后能在字节流里找到口令(字段不是 transient)
2 次 vs 1 次
两个 required 模块各向 handler 要一次口令;A 存口令、B 用 useFirstPass 时只要一次

LoginContext、LoginModule 与 CallbackHandler

实测 · 本机执行输出
  [flow]
    好口令:成功;Subject 的主体 [P(amy@A)];logout 之后主体 []
          · A.initialize options={id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证通过
          · A.commit
          · A.logout
          handler 被调用 1 次
    错口令:失败 FailedLoginException: A: 用户名或口令不对
          · A.initialize options={id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证失败
          · A.abort
          handler 被调用 1 次

PasswordCallback 自身

实测 · 本机执行输出
  [pc]
    getPrompt = “口令: ”;isEchoOn = false;未设置时 getPassword = null
    setPassword 之后:getPassword() 两次返回的是同一个数组? false;与传入的数组是同一个? false;内容相同? true
    把传入的数组清成 x 之后:pc.getPassword() = hunter2(回调内部保存的是副本)
    改动 getPassword() 返回的数组之后,再取:hunter2(返回的也是副本)
    clearPassword() 之后 getPassword() 返回 长度 7 的数组,字符码集合 [32];之前取出的副本 g2 仍然是 hunter2(调用者自己取出的副本要自己清)
    setPassword(null) → 成功;getPassword = null
    new PasswordCallback(null, true) → IllegalArgumentException: null;new PasswordCallback("", true) → IllegalArgumentException: null
    echoOn = true 的回调:isEchoOn = true(只是给界面的提示,类本身不回显)
    可序列化吗? PasswordCallback implements Serializable = true;序列化一个设置了口令的回调(口令字段是否被序列化):序列化 170 字节;字节流里能找到口令的 UTF-16BE 形式? true;反序列化回来 getPassword = pw123456;inputPassword 字段是 transient? false

多个模块与共享状态

实测 · 本机执行输出
  [order]
    TwoRequired:成功;Subject 的主体 [P(amy@A), P(amy@B)];logout 之后主体 []
          · A.initialize options={id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证通过
          · B.initialize options={id=B} sharedState.keys=[]
          · B.login 向 handler 要了用户名与口令
          · B.login 验证通过
          · A.commit
          · B.commit
          · A.logout
          · B.logout
          handler 被调用 2 次
    TwoRequiredOneBad:失败 FailedLoginException: B: 用户名或口令不对
          · A.initialize options={id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证通过
          · B.initialize options={password=other, id=B} sharedState.keys=[]
          · B.login 向 handler 要了用户名与口令
          · B.login 验证失败
          · A.abort
          · B.abort
          handler 被调用 2 次
    SufficientFirst:成功;Subject 的主体 [P(amy@A)];logout 之后主体 []
          · A.initialize options={id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证通过
          · A.commit
          · A.logout
          · B.initialize options={password=other, id=B} sharedState.keys=[]
          · B.logout
          handler 被调用 1 次
    OptionalBad:成功;Subject 的主体 [P(amy@A), P(amy@B)];logout 之后主体 []
          · A.initialize options={password=other, id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证失败
          · B.initialize options={id=B} sharedState.keys=[]
          · B.login 向 handler 要了用户名与口令
          · B.login 验证通过
          · A.commit
          · B.commit
          · A.logout
          · B.logout
          handler 被调用 2 次
    RequisiteBad:失败 FailedLoginException: A: 用户名或口令不对
          · A.initialize options={password=other, id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证失败
          · A.abort
          · B.initialize options={id=B} sharedState.keys=[]
          · B.abort
          handler 被调用 1 次
    SharedState:成功;Subject 的主体 [P(amy@A), P(amy@B)];logout 之后主体 []
          · A.initialize options={storePass=true, id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证通过
          · B.initialize options={id=B, useFirstPass=true} sharedState.keys=[javax.security.auth.login.password, javax.security.auth.login.name]
          · B.login 使用共享状态里的口令(没有再向 handler 要)
          · B.login 验证通过
          · A.commit
          · B.commit
          · A.logout
          · B.logout
          handler 被调用 1 次
    SharedStateNoStore:成功;Subject 的主体 [P(amy@A), P(amy@B)];logout 之后主体 []
          · A.initialize options={id=A} sharedState.keys=[]
          · A.login 向 handler 要了用户名与口令
          · A.login 验证通过
          · B.initialize options={id=B, useFirstPass=true} sharedState.keys=[]
          · B.login 向 handler 要了用户名与口令
          · B.login 验证通过
          · A.commit
          · B.commit
          · A.logout
          · B.logout
          handler 被调用 2 次

各种错误

实测 · 本机执行输出
  [errors]
    handler 不支持 PasswordCallback:失败 LoginException: A: 回调失败 UnsupportedCallbackException: 只支持 NameCallback
    handler 抛 IOException:失败 LoginException: A: 回调失败 IOException: 终端不可用
    handler 抛运行时异常:失败 LoginException: java.lang.IllegalStateException: handler 坏了(消息里还附带 15 行堆栈文本)
    handler 为 null:LoginException: invalid null CallbackHandler provided
    不存在的配置项:LoginException: No LoginModules configured for Nope
    模块类不存在:失败 LoginException: No LoginModule found for pk.NoSuchModule
    模块类不是 public:失败 LoginException: java.lang.IllegalAccessException: class javax.security.auth.login.LoginContext (in module java.base) cannot access a member of class pk.Hidden with package access(消息里还附带 15 行堆栈文本)
    login() 两次:第二次成功;主体 [P(amy@A), P(amy@A)]
    logout() 之前没有 login:LoginException: null subject - logout called before login
    getSubject() 在 login 之前 / 失败之后:login 前 null;失败后 null
    没有 handler 且模块需要回调:LoginException: java.lang.NullPointerException: Cannot invoke "javax.security.auth.callback.CallbackHandler.handle(javax.security.auth.callback.Callback[])" because "this.handler" is null(消息里附带堆栈文本)

KeyStoreLoginModule 的回调

实测 · 本机执行输出
  [keystore]
    KeyStoreLoginModule 登录成功;Subject 主体 [X500Principal(CN=localhost)];公开凭证 [X509CertPath];私有凭证 [X500PrivateCredential]
    logout 之后私有凭证 0 个,公开凭证 0 个
    回调的顺序与内容:
          · TextOutputCallback(0, "Please enter keystore information")
          · NameCallback("Keystore alias: ", default=srv)
          · PasswordCallback("Keystore password: ", echo=false)
          · PasswordCallback("Private key password (optional): ", echo=false)
          · ConfirmationCallback(null)
    口令错误:LoginException: Error initializing keystore ← IOException: keystore password was incorrect

分步演示

PasswordCallback
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;JAAS 配置来自 -Djava.security.auth.login.config 指向的临时文件;DemoModule 与 handler 是为实验自写的
    • 没有验证:Krb5LoginModule / JndiLoginModule / LdapLoginModule 的回调(需要 KDC / LDAP)、PKCS11 KeyStoreLoginModule、控制台 handler(TextCallbackHandler)的回显行为(需要交互终端)、在 SecurityManager 已移除的 JDK 里 doAs 与 Subject.callAs 的细节、口令在堆中的残留(没有做堆转储检查)
    京ICP备2026067502号-1京公网安备11010502063487号