Java安全之認證與授權
Java平臺提供的認證與授權服務(Java Authentication and Authorization Service (JAAS)),能夠控制代碼對敏感或關鍵資源的訪問,例如文件系統,網絡服務,系統屬性訪問等,加強代碼的安全性。主要包含認證與授權兩部分,認證的目的在於可靠安全地確定當前是誰在執行代碼,代碼可以是一個應用,applet,bean,servlet;授權的目的在於確定了當前執行代碼的用戶有什麼權限,資源是否可以進行訪問。雖然JAAS表面上分爲了兩大部分,而實際上兩者是密不可分的,下面看一段代碼:
public class App {
public static void main(String[] args) {
System.out.println(System.getProperty("java.home"));
}
}
非常簡單只是輸出java.home系統屬性,現在肯定是沒有任何問題,屬性會能正常輸出。把上述代碼改爲如下後:
public class App {
public static void main(String[] args) {
//安裝安全管理器
System.setSecurityManager(new SecurityManager());
System.out.println(System.getProperty("java.home"));
}
}
拋出瞭如下異常:java.security.AccessControlException: access denied (“java.util.PropertyPermission” “java.home” “read”),異常提示沒有對java.home的讀取權限,系統屬性也是一種資源,與文件訪問類似;默認情況下對於普通Java應用是沒有安裝安全管理器,在手動安裝安全管理器後,如果沒有爲應用授權則沒有任何權限,所以應用無法訪問java.home系統屬性。
授權的方式是爲安全管理器綁定一個授權策略文件。由於我是在eclipse Java工程中直接運行main方法,所以就在工程根目錄下新建一個demo.policy文件,文件內容如下:
grant {
permission java.util.PropertyPermission "java.home", "read";
};
該授權的效果是任何用戶運行的任何程序都有對java.home的讀權限,policy文件的具體格式請參看:http://docs.oracle.com/javase/7/docs/technotes/guides/security/PolicyFiles.html
爲安全管理器綁定policy文件的方式有兩種:一、在運行程序的時候加入-Djava.security.policy=demo.policy虛擬機啓動參數;二、執行System.setProperty(“java.security.policy”, “demo.policy”);其實兩者的效果一樣,都是在設置系統屬性,其中demo.policy是路徑,這裏爲了簡單指定的是相對路徑,絕對路徑當然也沒問題。再次運行程序不再拋出異常,說明程序擁有了對java.home系統屬性的讀取權限。在Java中權限有很多,具體可參考:http://docs.oracle.com/javase/7/docs/technotes/guides/security/spec/security-spec.doc3.html#17001
在上述過程中雖然完成了授權,但授權的針對性不強,在程序綁定了該policy文件後,無論是哪個用戶執行都將擁有java.home系統屬性的讀權限,現在我們希望做更加細粒度的權限控制,這裏需要用到認證服務了。
認證服務有點“麻煩”,一般情況下主要都涉及到了LoginContext,LoginModule,CallbackHandler,Principal,後三者還需要開發者自己實現。這裏先解釋一下這幾個類的作用:
1.LoginContext:認證核心類,也是入口類,用於觸發登錄認證,具體的登錄模塊由構造方法name參數指定
2.LoginModule:登錄模塊,封裝具體的登錄認證邏輯,如果認證失敗則拋出異常,成爲則向Subject中添加一個Principal
3.CallbackHandler:回調處理器,用於蒐集認證信息
4.Principal:代表程序用戶的某一身份,與其密切相關的爲Subject,用於代表程序用戶,而一個用戶可以多種身份,授權時可以針對某用戶的多個身份分別授權
下面看一個認證例子:
package com.xtayfjpk.security.jaas.demo;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
public class App {
public static void main(String[] args) {
System.setProperty("java.security.auth.login.config", "demo.config");
System.setProperty("java.security.policy", "demo.policy");
System.setSecurityManager(new SecurityManager());
try {
//創建登錄上下文
LoginContext context = new LoginContext("demo", new DemoCallbackHander());
//進行登錄,登錄不成功則系統退出
context.login();
} catch (LoginException le) {
System.err.println("Cannot create LoginContext. " + le.getMessage());
System.exit(-1);
} catch (SecurityException se) {
System.err.println("Cannot create LoginContext. " + se.getMessage());
System.exit(-1);
}
//訪問資源
System.out.println(System.getProperty("java.home"));
}
}
package com.xtayfjpk.security.jaas.demo;
import java.io.IOException;
import java.security.Principal;
import java.util.Iterator;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
public class DemoLoginModule implements LoginModule {
private Subject subject;
private CallbackHandler callbackHandler;
private boolean success = false;
private String user;
private String password;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
}
@Override
public boolean login() throws LoginException {
NameCallback nameCallback = new NameCallback("請輸入用戶名");
PasswordCallback passwordCallback = new PasswordCallback("請輸入密碼", false);
Callback[] callbacks = new Callback[]{nameCallback, passwordCallback};
try {
//執行回調,回調過程中獲取用戶名與密碼
callbackHandler.handle(callbacks);
//得到用戶名與密碼
user = nameCallback.getName();
password = new String(passwordCallback.getPassword());
} catch (IOException | UnsupportedCallbackException e) {
success = false;
throw new FailedLoginException("用戶名或密碼獲取失敗");
}
//爲簡單起見認證條件寫死
if(user.length()>3 && password.length()>3) {
success = true;//認證成功
}
return true;
}
@Override
public boolean commit() throws LoginException {
if(!success) {
return false;
} else {
//如果認證成功則得subject中添加一個Principal對象
//這樣某身份用戶就認證通過並登錄了該應用,即表明了誰在執行該程序
this.subject.getPrincipals().add(new DemoPrincipal(user));
return true;
}
}
@Override
public boolean abort() throws LoginException {
logout();
return true;
}
@Override
public boolean logout() throws LoginException {
//退出時將相應的Principal對象從subject中移除
Iterator<Principal> iter = subject.getPrincipals().iterator();
while(iter.hasNext()) {
Principal principal = iter.next();
if(principal instanceof DemoPrincipal) {
if(principal.getName().equals(user)) {
iter.remove();
break;
}
}
}
return true;
}
}
package com.xtayfjpk.security.jaas.demo;
import java.io.IOException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
public class DemoCallbackHander implements CallbackHandler {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
NameCallback nameCallback = (NameCallback) callbacks[0];
PasswordCallback passwordCallback = (PasswordCallback) callbacks[1];
//設置用戶名與密碼
nameCallback.setName(getUserFromSomeWhere());
passwordCallback.setPassword(getPasswordFromSomeWhere().toCharArray());
}
//爲簡單起見用戶名與密碼寫死直接返回,真實情況可以由用戶輸入等具體獲取
public String getUserFromSomeWhere() {
return "zhangsan";
}
public String getPasswordFromSomeWhere() {
return "zhangsan";
}
}
package com.xtayfjpk.security.jaas.demo;
import java.security.Principal;
public class DemoPrincipal implements Principal {
private String name;
public DemoPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
使用認證服務時,需要綁定一個認證配置文件,在例子中通過System.setProperty(“java.security.auth.login.config”, “demo.config”);實現,當然也可以設置虛擬屬性-Djava.security.auth.login.config=demo.config實現。配置文件內容如下:
demo {
com.xtayfjpk.security.jaas.demo.DemoLoginModule required debug=true;
};
其中demo爲配置名稱,其內容指定了需要使用到哪登錄模塊(LoginModule),認證配置文件具體格式請參看:http://docs.oracle.com/javase/6/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html
前面說到認證與授權密不可分,這裏就可以說明,在創建LoginContext對象時就需要有createLoginContext.demo的認證權限,demo就是認證配置文件中的配置名稱,該名稱在構造LoginContext對象時指定。由於在DemoLoginModule中修改了Subject的principals集合,還需要有modifyPrincipals認證權限,所以授權策略文件內容變爲:
grant {
permission javax.security.auth.AuthPermission "createLoginContext.demo";
permission javax.security.auth.AuthPermission "modifyPrincipals";
permission java.util.PropertyPermission "java.home", "read";
};
再次運行程序,java.home系統屬性正常輸出,但此時我們還是沒有針對某特定用戶身份進行授權,這個就需要在授權文件中配置Principal,現在將授權文件改寫爲:
grant principal com.xtayfjpk.security.jaas.demo.DemoPrincipal "zhangsan"{
permission java.util.PropertyPermission "java.home", "read";
};
grant {
permission javax.security.auth.AuthPermission "createLoginContext.demo";
permission javax.security.auth.AuthPermission "modifyPrincipals";
permission javax.security.auth.AuthPermission "doAsPrivileged";
};
這就意味着只有以名爲zhangsan的DemoPrincipal登錄應用纔會擁有java.home系統屬性的讀權限,此時讀取java.home的代碼需要做一定的修改,如下:
Subject subject = context.getSubject();
//該方法調用需要"doAsPrivileged"權限
Subject.doAsPrivileged(subject, new PrivilegedAction<Object>() {
@Override
public Object run() {
System.out.println(System.getProperty("java.home"));
return null;
}
}, null);
因爲在Subject中才有Principal信息,這樣就可以針對每一種用戶身份制定一套權限方案。