Java 安全模型經歷三代演進:沙箱 (JDK 1.0) → 簽名程式碼分級信任 (JDK 1.1) → **可配置安全策略 + JAAS 雙層授權 (Java 2)**。本文完整實作一個基於 JAAS 的登入認證與檔案存取授權範例,涵蓋 Principal、LoginModule、CallbackHandler、Policy 配置全鏈路。
類別/介面 套件 角色
Subject javax.security.auth 認證後的主體實體,攜帶 Principals 與 Credentials
Principal java.security 身份標識 (用戶名、角色、群組)
LoginContext javax.security.auth.login 登入流程協調器,讀取 login.conf 載入 LoginModule
LoginModule javax.security.auth.spi 認證邏輯實作 (initialize, login, commit, abort, logout)
CallbackHandler javax.security.auth.callback 互動介面适配器 (GUI/CLI/Web 取得帳密)
PrivilegedAction java.security 授權執行單元,在 Subject.doAs() 中運行
完整實作程式碼
1. Principal 實作
package com.jungleford.auth;
import java.security.Principal;
public class MyPrincipal implements Principal {
private final String name;
public MyPrincipal(String name) { this.name = name; }
public String getName() { return name; }
public boolean equals(Object o) { return o instanceof MyPrincipal && name.equals(((MyPrincipal)o).getName()); }
public String toString() { return "MyPrincipal:" + name; }
public int hashCode() { return name.hashCode(); }
}
2. LoginModule 實作 (兩階段提交)
package com.jungleford.auth;
import javax.security.auth.*;
import javax.security.auth.callback.*;
import javax.security.auth.login.*;
import javax.security.auth.spi.*;
import java.util.*;
import java.io.IOException;
public class MyLoginModule implements LoginModule {
private Subject subject;
private CallbackHandler cbHandler;
private Map sharedState, options;
private boolean loginSucceeded = false, commitSucceeded = false;
private String username;
private char[] password;
private Principal principal;
public void initialize(Subject subject, CallbackHandler cbHandler,
Map sharedState, Map options) {
this.subject = subject; this.cbHandler = cbHandler;
this.sharedState = sharedState; this.options = options;
System.out.println("Login module initializing...");
}
public boolean login() throws LoginException {
if (cbHandler == null) throw new LoginException("No CallbackHandler");
Callback[] cbs = { new NameCallback("Login: "), new PasswordCallback("Password: ", false) };
try {
cbHandler.handle(cbs);
username = ((NameCallback)cbs[0]).getName();
char[] tmp = ((PasswordCallback)cbs[1]).getPassword();
password = (tmp == null) ? new char[0] : tmp.clone();
((PasswordCallback)cbs[1]).clearPassword();
} catch (IOException|UnsupportedCallbackException e) {
throw new LoginException(e.toString());
}
// 簡單驗證:user / letmepass
if ("user".equals(username) && Arrays.equals(password, "letmepass".toCharArray())) {
System.out.println("MyLoginModule: Authentication pass!");
loginSucceeded = true;
return true;
} else {
System.out.println("MyLoginModule: Authentication failed!");
loginSucceeded = false;
cleanup();
throw new FailedLoginException("user".equals(username) ? "Password incorrect!" : "Username incorrect!");
}
}
public boolean commit() throws LoginException {
if (!loginSucceeded) return false;
principal = new MyPrincipal(username);
subject.getPrincipals().add(principal);
System.out.println("MyLoginModule: Add a new principal to current subject.");
cleanup();
commitSucceeded = true;
return true;
}
public boolean abort() throws LoginException {
if (loginSucceeded && !commitSucceeded) {
loginSucceeded = false; cleanup(); principal = null;
} else if (commitSucceeded) {
logout();
}
return true;
}
public boolean logout() throws LoginException {
subject.getPrincipals().remove(principal);
loginSucceeded = commitSucceeded = false;
cleanup();
principal = null;
return true;
}
private void cleanup() {
if (username != null) username = null;
if (password != null) { Arrays.fill(password, ' '); password = null; }
}
}
3. CallbackHandler 實作 (Swing GUI)
package com.jungleford.auth;
import javax.security.auth.callback.*;
import javax.swing.*;
import java.io.IOException;
public class MyCallbackHandler implements CallbackHandler {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
String username = JOptionPane.showInputDialog(null,
"Available name: user", "Enter your name", JOptionPane.QUESTION_MESSAGE);
String password = JOptionPane.showInputDialog(null,
"Available password: letmepass", "Enter your password", JOptionPane.QUESTION_MESSAGE);
for (Callback cb : callbacks) {
if (cb instanceof NameCallback) ((NameCallback)cb).setName(username);
else if (cb instanceof PasswordCallback) ((PasswordCallback)cb).setPassword(password.toCharArray());
else if (cb instanceof TextOutputCallback) {
TextOutputCallback toc = (TextOutputCallback)cb;
switch (toc.getMessageType()) {
case TextOutputCallback.INFORMATION: System.out.println(toc.getMessage()); break;
case TextOutputCallback.ERROR: System.err.println("Error: " + toc.getMessage()); break;
case TextOutputCallback.WARNING: System.err.println("Warning: " + toc.getMessage()); break;
default: throw new IOException("Unsupported message type");
}
} else throw new UnsupportedCallbackException(cb, "Unrecognized Callback");
}
}
}
4. PrivilegedAction 實作 (受保護資源存取)
package com.jungleford.auth;
import java.io.*;
import java.security.PrivilegedAction;
public class MyAction implements PrivilegedAction {
public Object run() {
File file = new File("myfile.txt");
StringBuilder content = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while ((line = br.readLine()) != null) content.append(line).append("\n");
} catch (Exception e) {
System.err.println("Error reading file"); e.printStackTrace();
}
return content.toString();
}
}
5. 主程式啟動
package com.jungleford.auth;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
public class JAASTest {
public static void main(String[] args) {
LoginContext lc;
try {
lc = new LoginContext("JAASTest", new MyCallbackHandler());
} catch (Exception e) { e.printStackTrace(); System.exit(-1); return; }
try { lc.login(); }
catch (Exception e) { e.printStackTrace(); System.exit(-1); return; }
Object result = Subject.doAs(lc.getSubject(), new MyAction());
System.out.println("Access successfully! Reading file:\n==================================");
System.out.println(result);
System.out.println("==================================");
}
}
配置檔案
login.conf (登入模組配置)
JAASTest {
com.jungleford.auth.MyLoginModule required;
};
required:此模組必須成功,整體登入才算成功 (其他 flag: requisite, sufficient, optional)。
jaas.policy (授權策略)
grant {
permission javax.security.auth.AuthPermission "createLoginContext";
permission javax.security.auth.AuthPermission "doAs";
permission javax.security.auth.AuthPermission "modifyPrincipals";
permission javax.security.auth.AuthPermission "getSubject";
};
grant principal com.jungleford.auth.MyPrincipal "user" {
permission java.io.FilePermission "myfile.txt", "read";
};
第一組 grant:授予代碼基礎權限 (建立 LoginContext、執行 doAs 等)。
第二組 grant principal:**基於身份授權**,擁有 MyPrincipal("user") 的 Subject 可讀取 myfile.txt。
啟動腳本 (JAASTest.bat)
java -Djava.security.manager \
-Djava.security.auth.login.config=login.conf \
-Djava.security.policy=jaas.policy \
com.jungleford.auth.JAASTest
受保護資源 (myfile.txt)
Why? Because they care!
Because they want to know the truth!
Because they want their country back!
Because it still belongs to us as long as the people have the guts to fight for what they believe in!
執行流程圖解
JAASTest.main()
│
▼
new LoginContext("JAASTest", MyCallbackHandler)
│ 讀取 login.conf → 實例化 MyLoginModule
▼
lc.login()
│ MyLoginModule.initialize()
│ MyLoginModule.login() → CallbackHandler 取得帳密 → 驗證
│ 成功 → loginSucceeded=true
▼
lc.login() 返回 → commit() 階段
│ MyLoginModule.commit() → Principal 加入 Subject
▼
Subject.doAs(subject, MyAction)
│ 檢查 jaas.policy: Principal 擁有 FilePermission?
│ 是 → 執行 MyAction.run() 讀取檔案
▼
輸出檔案內容
關鍵設計精華
認證與授權分離 :LoginModule 僅負責驗證身份;Policy 檔案聲明身份擁有何種權限。
兩階段提交 :login() 驗證、commit() 確認將 Principal 加入 Subject,支援多模組協同決策。
CallbackHandler 解耦互動 :同一 LoginModule 可搭配 CLI、Swing、Web (Filter) 不同 CallbackHandler。
代碼基礎 vs 身份基礎授權 :grant codeBase 保護系統完整性;grant principal 實現業務層面 RBAC。
最小權限原則 :Policy 僅授予必要權限 (此處僅 read myfile.txt),而非 AllPermission。
生產環境擴充建議
LoginModule 對接 LDAP/Database/Kerberos 實現企業級身分來源。
Policy 改用資料庫儲存,配合 Policy.refresh() 熱更新。
整合 Spring Security / Apache Shiro 獲得更高層抽象 (URL 保護、註解式授權、Session 管理)。
審計日誌:在 PrivilegedAction 或自定義 SecurityManager 記錄敏感操作。
上一篇文章: Java 區域變數作用域陷阱:編譯期「可能未初始化」錯誤原因與三種修正模式 下一篇文章: 《Java 深度歷險》讀書筆記 (一):JRE 尋找邏輯、類別載入器層次與 JNI 實戰
Copyright ©
程式碼之家 All Rights Reserved