一、Java安全架構
Java從誕生開始就是面向網絡應用,JVM需要加載網絡傳輸過來的.class文件,即會加載遠程類,因此遠程類中對系統資源的訪問必須做限制,也就是所謂“沙盒模型”,將遠程代碼可執行的權限限制在沙盒中。
二、jdk安全檢查接口
2.1 Permission
Permission類代表一個權限,其中最重要的幾個方法如下:
方法 | 描述 |
---|---|
boolean implies(Permission permission) | 當前權限是否包含傳入的權限 |
String getName() | 權限作用的目標,如文件權限,這裏爲文件路徑 |
String getActions() | 對目標可執行操作的集合,英文逗號分隔,文件權限如"read,write" |
注意Permission代表可執行的權限集合,沒有代表拒絕的Permission。
Permission使用示例:
//檢查是否有讀 d:/test.txt 文件的權限
AccessController.checkPermission(new FilePermission("d:/test.txt", "read"));
事實上,jdk的FileInputStream就執行了安全檢查:
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
//...
最終,會調用到AccessController.checkPermission方法。
不過,jdk默認是沒有安裝SecurityManager的,這一點後面會說明,因此平常你寫的java程序可以訪問系統上任何文件不受限制,除非操作系統對用戶權限做了限制。
2.2 CodeSource
CodeSource代表代碼來源:
方法 | 描述 |
---|---|
CodeSource(URL url, java.security.cert.Certificate certs[]) | 構造方法描述了兩個主要的屬性。URL代表代碼來源,可能來源於網絡,如”http://foo/foo.jar",也可能來着本地文件系統,如"file:/repository/foo.jar"。Certificate代表簽名代碼的證書,一份代碼可被多個私鑰進行簽名,此時通過公鑰(X509證書)可以驗證代碼是否可信。 |
2.3 AccessController
AccessController是執行權限檢查的接口,主要方法:
方法 | 描述 |
---|---|
public static void checkPermission(Permission perm) | 執行權限檢查 |
public static native T doPrivileged(PrivilegedAction action) | 在特權模式中執行權限檢查 |
如前所述,傳入構造號的Permission對象,系統檢查是否擁有對應權限,否則將拋出SecurityException,這是個RuntimeException的子類:
AccessController.checkPermission(new FilePermission("d:/test.txt", "read"));
那麼,特權模式的意義是什麼呢?
理解這個問題,要結合前面CodeSource的概念。假設class A來自file:/a.jar,class B來自file:/b.jar,定義如下:
public class A {
public void a() {
AccessController.checkPermission(new FilePermission("d:/test.txt", "read"));
}
}
public class B {
public void b() {
A a = new A();
//將拋出SecurityException
a.a();
}
}
假設系統賦予了來自a.jar的class讀取 “d:/test.txt” 文件的權限,而來自b.jar的class則沒有權限,那麼當調用B.b(),checkPermission是否應該通過呢?
答案是:否。
這裏奉行的是 最小權限原則,否則來自遠程的不可信代碼,通過調用系統代碼,就可以"爲所欲爲"了。
問題是,如果A.a()確實有訪問"d:/test.txt"的需求,並且確定訪問該文件不會對系統安全造成影響,該怎麼辦呢?
答案是使用特權模式,在這種模式下,只會考慮直接調用者擁有的權限,不會考慮上層調用者是否擁有相應的權限。
因此可以改成這樣:
public class A {
public void a() {
//在特權模式中執行權限檢查,不考慮上層調用者
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
//由於a.jar被賦予了讀取權限,此處不會拋出異常
AccessController.checkPermission(new FilePermission("d:/test.txt", "read"));
//執行讀文件操作...
return null;
}
});
}
}
public class B {
public void b() {
A a = new A();
//可以正常調用,雖然b.jar沒有讀d:/test.txt文件的權限
a.a();
}
}
2.4 SecurityManager
SecurityManager其實是AccessController的門面類,當然也可以自己定製SecurityManager,替換系統默認實現,默認則是調用AccessController執行安全檢查。
系統默認不啓用SecurityManager,因此調用System.getSecurtityManager()將返回null。開啓安全管理器需要添加系統屬性:
-Djava.security.manager
基本調用方式:
SecurityManager security = System.getSecurityManager();
//檢查系統是否安裝了SecurityManager
if (security != null) {
security.checkPermission(permission);
}
2.5 Policy
講了半天安全檢查,那麼,系統是怎麼確定代碼權限的呢?答案是通過policy文件。
policy文件位置查找:
- jdk/jre/lib/security/java.security中,policy.url.n屬性
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy
- 通過系統屬性指定:
-Djava.security.policy=d:my.policy
policy文件格式,以下面將用到的my.policy文件爲例:
grant codeBase "file:///D:/spaces/ssp/securityTest/bin" {
permission "java.lang.RuntimePermission" "accessDeclaredMembers";
permission "java.lang.RuntimePermission" "createClassLoader";
permission "java.lang.RuntimePermission" "closeClassLoader";
permission java.io.FilePermission "d:/test.txt", "read";
permission java.io.FilePermission "d:/testjar/test.jar", "read";
permission otaku.security.MyPermission "hello", "action1,action2";
};
grant codeBase "file:///d:/testjar/test.jar" {
permission java.io.FilePermission "d:/test.jar", "read";
};
基本格式:
grant codeBase <代碼位置> {
permission <權限類全類名> <目標名> <允許的操作集合>;
...
}
三、基本使用
3.1系統架構
其中SecurityTest是主類,MyPermission是自定義的權限類,使用的policy文件爲d:/my.policy,內容如下:
grant codeBase "file:///D:/spaces/ssp/securityTest/bin" {
permission "java.lang.RuntimePermission" "accessDeclaredMembers";
permission "java.lang.RuntimePermission" "createClassLoader";
permission "java.lang.RuntimePermission" "closeClassLoader";
permission java.io.FilePermission "d:/test.txt", "read";
permission java.io.FilePermission "d:/testjar/test.jar", "read";
permission otaku.security.MyPermission "hello", "action1,action2";
};
grant codeBase "file:///d:/testjar/test.jar" {
permission java.io.FilePermission "d:/test.jar", "read";
};
啓動參數:
-Djava.security.manager -Djava.security.policy=d:/my.policy
3.2代碼實現
3.2.1 MyPermission
自定義權限類,擴展權限系統。
package otaku.security;
import java.security.Permission;
import java.util.Arrays;
import java.util.List;
/**
* 自定義權限類,通過name和actions確定是否擁有相關權限
* TODO 考慮序列化安全
* @author ljf
*
*/
public class MyPermission extends Permission {
private static final long serialVersionUID = 1L;
private String actionStr;
private List<String> actions;
public MyPermission(String name, String actions) {
super(name);
this.actionStr =actions;
this.actions = Arrays.asList(actions.split(","));
}
@Override
public boolean implies(Permission other) {
if (!(other instanceof MyPermission)) {
return false;
}
MyPermission otherMy = (MyPermission)other;
return getName().equals(other.getName()) && actions.containsAll(otherMy.actions);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof MyPermission)) {
return false;
}
MyPermission otherMy = (MyPermission)other;
if (!otherMy.getName().equals(getName())) {
return false;
}
if (!(actions.size() == otherMy.actions.size() && actions.containsAll(otherMy.actions))) {
return false;
}
return true;
}
@Override
public int hashCode() {
return actionStr.hashCode();
}
@Override
public String getActions() {
return actionStr;
}
}
3.2.2 TestUtil
來自d:/testjar/test.jar的class,擁有讀d:/test.jar的權限。
package otaku.security;
import java.io.FilePermission;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
public class TestUtil {
public static void read() throws PrivilegedActionException {
//必須在特權模式下執行,因爲上層調用者沒有相關權限
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws Exception {
AccessController.checkPermission(new FilePermission("d:/test.jar", "read"));
return null;
}
});
}
}
3.2.3 SecurityTest
程序主類:
package otaku.security;
import java.io.FilePermission;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
public class SecurityTest {
public static void main(String[] args) throws Exception {
AccessController.checkPermission(new FilePermission("d:/test.txt", "read"));
AccessController.checkPermission(new MyPermission("hello", "action1"));
AccessController.checkPermission(new MyPermission("hello", "action1,action2"));
try {
//policy中沒賦予action3
AccessController.checkPermission(new MyPermission("hello", "action1,action2, action3"));
} catch (SecurityException e) {
e.printStackTrace();
}
//加載其他代碼源
URLClassLoader classLoader = new URLClassLoader(new URL[] {
new URL("file:///d:/testjar/test.jar")
});
Class<?> clazz = classLoader.loadClass("otaku.security.TestUtil");
Method method = clazz.getDeclaredMethod("read");
method.invoke(null);
classLoader.close();
System.out.println("over");
}
}
執行結果:
權限系統運行正常,驗證完畢。
四、參考書目
<<Java2平臺安全技術>> by 宮力。