Java安全与应用部署全解析
1. Java安全基础
1.1 消息认证码计算
消息认证码(MAC)用于验证数据的完整性和真实性。以下是一个创建
Mac
类实例并计算数据消息认证码的示例代码:
import java.security.*;
import javax.crypto.*;
import java.io.*;
public class MacExample {
public static void main(String args[]) {
try {
String inputString = "Test input string";
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey secretKey = keyGen.generateKey();
Mac mac = Mac.getInstance(secretKey.getAlgorithm());
mac.init(secretKey);
// the Mac class needs data in byte format
byte[] byteData = inputString.getBytes("UTF8");
// Compute the MAC for the data all in one operation
byte[] macBytes = mac.doFinal(byteData);
String macAsString = new sun.misc.BASE64Encoder().encode(macBytes);
System.out.println(
"The computed message authentication code is: " + macAsString);
} catch (InvalidKeyException e) {
} catch (NoSuchAlgorithmException e) {
} catch (UnsupportedEncodingException e) {
}
}
}
计算消息认证码与使用
MessageDigest
引擎类计算消息摘要非常相似,主要区别在于需要一个密钥。在上述示例中,密钥通过
KeyGenerator
生成,通常这个密钥会被保存或传输,用于验证 MAC 是否与数据匹配。
1.2 JAAS简介
JAAS(Java Authentication and Authorization Service)用于提供身份验证和授权服务,它在 JDK 1.4 版本中成为 J2SDK 的一部分。身份验证是验证应用程序用户(包括 applet、servlet 等任何类型的 Java 程序)的过程,而授权是为经过身份验证的用户授予执行操作(如修改特定访问控制文件)权限的过程。身份验证和授权共同为程序提供访问控制。
1.2.1 用户标识
为了实现访问控制,需要一种存储用户身份的方法,这通过
Subject
来完成。
Subject
是一组信息,用于标识所有请求的来源,例如登录到系统的特定用户。
Subject
具有关联的主体(principals)和公共、私有凭据(credentials)。
Subject
提供了两个构造函数:
public Subject();
public Subject(boolean readOnly, Set principals, Set pubCredentials, Set privCredentials);
第一个构造函数创建一个非只读的
Subject
,其主体、公共和私有凭据集为空(非 null);第二个构造函数展示了
Subject
所包含的信息。
Subject
还提供了以下方法来更改和检索只读状态:
public void setReadOnly();
public boolean isReadOnly();
以及以下方法来检索主体、公共或私有凭据集的句柄:
public Set getPrincipals();
public Set getPrincipals(Class c);
public Set getPublicCredentials();
public Set getPublicCredentials(Class c);
public Set getPrivateCredentials();
public Set getPrivateCredentials(Class c);
每个访问方法的版本都有一个
Class
参数,允许仅检索特定类型的主体/凭据,但这些方法返回的是一个新集,与
Subject
的内部集不对应。
Subject
可以与
AccessControlContext
关联,通过以下静态方法访问:
public static Subject getSubject(final AccessControlContext acc);
1.2.2 执行带安全检查的代码
Subject
类提供了
doAs
和
doAsPrivileged
方法来执行包含安全限制的代码。要使用这些方法,需要实现
java.security.PrivilegedAction
接口,该接口只定义了一个方法:
public Object run();
doAs
方法的定义如下:
public static Object doAs(final Subject subject, final java.security.PrivilegedAction action);
public static Object doAs(final Subject subject, final java.security.PrivilegedExceptionAction action) throws java.security.PrivilegedActionException;
doAsPrivileged
方法的定义如下:
public static Object doAsPrivileged(final Subject subject, final java.security.PrivilegedAction action, final java.security.AccessControlContext acc);
public static Object doAsPrivileged(final Subject subject, final java.security.PrivilegedExceptionAction action, final java.security.AccessControlContext acc) throws java.security.PrivilegedActionException;
这些方法首先将
Subject
与当前线程的
AccessControlContext
关联,然后执行操作。
doAsPrivileged
方法允许指定要使用的
AccessControlContext
,而不是使用当前线程的。
1.2.3 主体(Principals)
主体可以是任何继承自
java.security.Principal
和
java.io.Serializable
的类类型。
Principal
接口定义了以下方法:
boolean equals(Object another);
String toString();
int hashCode();
String getName();
当一个实体进行身份验证时,必须提供凭据(安全系统用于验证该实体的信息),例如用户登录系统时需要提供用户名和密码。
1.2.4 凭据(Credentials)
凭据可以是任何类型,JAAS 提供了
Refreshable
和
Destroyable
两个接口,为凭据类赋予了一些有用的行为。
javax.security.auth.Refreshable
接口适用于需要刷新状态的凭据,定义了以下方法:
boolean isCurrent();
void refresh() throws RefreshFailedException;
javax.security.auth.Destroyable
接口为凭据提供了销毁内容的语义,定义了以下方法:
boolean isDestroyed();
void destroy() throws DestroyFailedException;
调用
destroy
方法后,需要凭据内容有效的方法应抛出
IllegalStateException
。
1.2.5 身份验证主体
主体的基本身份验证方式是通过
LoginContext
对象,其身份验证步骤如下:
1. 实例化
LoginContext
对象。
2.
LoginContext
咨询
Configuration
以加载当前应用程序的所有
LoginModule
。
3. 调用
LoginContext
的
login
方法。
4. 每个
LoginModule
尝试对主体进行身份验证,并将主体/凭据与成功验证的用户关联。
5. 将身份验证的成功或失败信息传达回应用程序。
1.2.6 配置文件
配置文件包含每个应用程序的多个身份验证配置,每个配置有一个名称(通常是应用程序名称)和用于身份验证的登录模块列表。配置可以有一组名为
other
的登录模块,用于在没有其他名称匹配时指定身份验证方案。每个登录模块集遵循以下语法:
NAME {
LoginModuleClass FLAG ModuleOptions;
LoginModuleClass FLAG ModuleOptions;
}
LoginModuleClass
是
LoginModule
的完全限定名,
FLAG
可以是以下值之一:
| Flag Name | Description |
| — | — |
| Required | 该
LoginModule
必须成功,但如果失败,后续指定的
LoginModule
仍会执行。 |
| Requisite | 该
LoginModule
必须成功,如果失败,控制权返回给应用程序,不再执行其他
LoginModule
。 |
| Sufficient | 该
LoginModule
不必须成功,如果成功,控制权立即返回给应用程序;即使失败,也会继续执行列表中的其他
LoginModule
。 |
| Optional | 该
LoginModule
不必须成功,无论成功或失败,控制权都会继续传递到列表中的下一个。 |
ModuleOptions
是一个以空格分隔的登录模块特定的
name=value
对列表。
1.2.7 LoginContext
LoginContext
类提供了一种简洁的方式来对主体进行身份验证,同时将身份验证细节留给
LoginModule
。它提供了以下构造函数:
public LoginContext(String name) throws LoginException;
public LoginContext(String name, Subject subject) throws LoginException;
public LoginContext(String name, CallbackHandler callbackHandler) throws LoginException;
public LoginContext(String name, Subject subject, CallbackHandler callbackHandler) throws LoginException;
name
参数对应于应用程序使用的配置中的条目。如果
LoginModule
需要与用户通信,可以通过
CallbackHandler
实现,
CallbackHandler
接口定义了一个方法:
void handle(Callback[] callbacks) throws java.io.IOException, UnsupportedCallbackException;
LoginContext
还提供了
login
和
logout
方法:
public void login() throws LoginException;
public void logout() throws LoginException;
以下是一个应用程序登录、获取经过身份验证的主体并注销的示例代码:
LoginContext loginContext = new LoginContext("BasicConsoleLogin");
try {
loginContext.login(); // utilizes callbacks
Subject subject = loginContext.getSubject();
// ... execute specific application code here ...
loginContext.logout();
} catch(LoginException le) {
// authentication failed
}
1.2.8 授权
身份验证提供了一种黑白分明的安全方法,用户要么经过验证,要么未经过验证。JAAS 提供授权,用于为实体授予不同程度的访问权限。每个应用程序可以使用一个策略文件,其中包含各种目标的权限列表。
策略文件包含多个
grant
部分,用于为代码或主体授予权限,基本格式如下:
grant signedBy "signer_names",
codeBase "URL",
principal principal_class_name "principal_name",
principal principal_class_name "principal_name",
... {
permission permission_class_name "target_name", "action", signedBy "signer_names";
permission permission_class_name "target_name", "action", signedBy "signer_names";
...
};
signedBy
和
codeBase
最多只能指定一次,而
principal
元素可以指定多次,这些都是可选元素。如果不指定任何元素,指定的权限将应用于所有执行的代码,无论其来源如何。
例如,Java 自带的
jre/lib/security
目录下的
java.policy
文件有一个策略,为 Java 扩展打开了广泛的权限:
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
2. Java应用部署
2.1 Java类路径问题
Java 类路径是一个系统环境变量,用于指示 Java 虚拟机(VM)查找一组类和/或 JAR 文件,它决定了 VM 知道程序使用的代码所在位置。
类路径可能会带来一些问题:
-
难以定位类
:有时需要一个类,但不知道哪个 JAR 文件包含该类,可能会添加大量 JAR 文件到类路径中,却不确定哪些是不必要的。
-
版本冲突
:类路径中存在同一类的多个版本,可能会导致难以追踪的错误。
-
长度限制
:操作系统对环境变量的长度有限制,过长的类路径在开发和部署过程中都可能带来麻烦。
2.2 管理长类路径的建议
- 使用相对路径 :了解应用程序的执行位置,使用相对路径代替绝对路径。
- 合并 JAR 文件 :尝试将应用程序及其库合并到尽可能少的 JAR 文件中。
-
使用扩展目录
:将常用的实用 JAR 文件(如第三方 JAR 文件)放置在安装的 JRE 的扩展目录(默认是
lib/ext)中,这样这些 JAR 文件就不需要出现在类路径中,但要确保 JAR 文件放置在正确的 JRE 中。
2.3 类路径管理工具
2.3.1 ClassPathVerifier
ClassPathVerifier
是一个简单的实用工具,用于验证文件中存储的类列表是否存在于类路径中。代码如下:
import java.io.*;
public class ClassPathVerifier {
public static void main(String args[]) {
try {
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(args[0])));
String clsName="";
while( (clsName = br.readLine()) != null) {
try {
Class.forName(clsName);
System.out.print(".");
} catch(Exception e) {
System.out.println("\nNOT FOUND: " + clsName);
}
}
br.close();
} catch(IOException ioe) {
System.out.println("IOException: " + ioe);
ioe.printStackTrace();
}
}
}
该类使用
Class.forName
方法,如果没有抛出异常,则表示类已找到。对于每个成功加载的类,会打印一个句点。
2.3.2 ClassSearch
ClassSearch
用于查找包含特定类的 JAR 文件,不需要指定类的完全限定名,任何类名和包的部分都可以。代码如下:
import java.io.*;
import java.util.zip.*;
import java.util.StringTokenizer;
public class ClassSearch {
private String m_baseDirectory;
private String m_classToFind;
private int m_resultsCount=0;
public void searchJarFile(String filePath) {
try {
FileInputStream fis = new FileInputStream(filePath);
BufferedInputStream bis = new BufferedInputStream(fis);
ZipInputStream zis = new ZipInputStream(bis);
ZipEntry ze = null;
while((ze=zis.getNextEntry()) != null) {
if(ze.isDirectory()) {
continue;
}
if(ze.getName().indexOf(m_classToFind) != -1) {
System.out.println(" " + ze.getName() +
"\n (inside " + filePath + ")");
m_resultsCount++;
}
}
} catch(Exception e) {
System.out.println("Exception: " + e);
e.printStackTrace();
}
}
public void findHelper(File dir, int level) {
int i;
File[] subFiles;
subFiles = dir.listFiles();
if(subFiles == null) {
return;
}
for(i=0; i<subFiles.length; i++) {
if(subFiles[i].isFile()) {
if(subFiles[i].getName().toLowerCase().indexOf(".jar") != -1) {
// found a jar file, process it
searchJarFile(subFiles[i].getAbsolutePath());
}
} else if(subFiles[i].isDirectory()) {
// directory, so recur
findHelper(subFiles[i], level+1);
}
}
}
public void searchClassPath(String classToFind) {
String classPath = System.getProperty("java.class.path");
System.out.println("Searching classpath: " + classPath);
StringTokenizer st = new StringTokenizer(classPath, ";");
m_classToFind = classToFind;
while(st.hasMoreTokens()) {
String jarFileName = st.nextToken();
if(jarFileName != null &&
jarFileName.toLowerCase().indexOf(".jar") != -1) {
searchJarFile(jarFileName);
}
}
}
public void findClass(String baseDir, String classToFind) {
System.out.println("SEARCHING IN: " + baseDir);
m_baseDirectory = baseDir;
m_classToFind = classToFind;
m_classToFind = m_classToFind.replaceAll("\\.", "/");
File start = new File(m_baseDirectory);
System.out.println("SEARCHING FOR: " + m_classToFind);
System.out.println("\nSEARCH RESULTS:");
findHelper(start, 1);
if(m_resultsCount == 0) {
System.out.println("No results.");
}
}
public static void main(String args[]) {
if(args.length < 1 || args.length > 2) {
System.out.println("Incorrect program usage");
System.out.println(" java ClassSearch <base directory> <class to find>\n");
System.out.println(" searches all jar files beneath base directory for class\n");
System.out.println("");
System.out.println(" java ClassSearch <class to find>\n");
System.out.println(" searches all jar files in classpath for class\n");
System.exit(1);
}
ClassSearch cs = new ClassSearch();
if(args.length == 1) {
cs.searchClassPath(args[0]);
} else if(args.length == 2) {
cs.findClass(args[0], args[1]);
}
}
}
该类使用 Java 的 zip 库和
File
类的目录搜索功能,根据命令行指定的类/包进行搜索。例如,以下命令用于在指定目录下搜索
RSAPrivateKey
类:
c:\>java ClassSearch "c:\program files\java\jdk1.6.0" RSAPrivateKey
输出结果可能如下:
SEARCHING IN: c:\program files\java\jdk1.6.0
SEARCHING FOR: RSAPrivateKey
SEARCH RESULTS:
com/sun/deploy/security/MozillaJSSRSAPrivateKey.class
(inside c:\program files\java\jdk1.6.0\jre\lib\deploy.jar)
com/sun/deploy/security/MSCryptoRSAPrivateKey.class
(inside c:\program files\java\jdk1.6.0\jre\lib\deploy.jar)
sun/security/mscapi/RSAPrivateKey.class
(inside c:\program files\java\jdk1.6.0\jre\lib\ext\sunmscapi.jar)
sun/security/pkcs11/P11Key$P11RSAPrivateKey.class
(inside c:\program files\java\jdk1.6.0\jre\lib\ext\sunpkcs11.jar)
java/security/interfaces/RSAPrivateKey.class
通过以上介绍,我们了解了 Java 安全和应用部署的相关知识,包括消息认证码计算、JAAS 身份验证和授权、类路径管理等内容,这些知识对于开发和部署安全可靠的 Java 应用程序非常重要。
2.4 Java Web Start
Java Web Start 是一种允许用户通过网络直接启动 Java 应用程序的技术。它提供了一种方便的方式来部署和更新 Java 应用,用户无需手动下载和安装应用程序。
2.4.1 工作原理
Java Web Start 的工作流程如下:
graph LR
A[用户访问启动文件] --> B[Java Web Start 下载启动文件]
B --> C{检查缓存}
C -->|文件存在且未过期| D[从缓存加载应用]
C -->|文件不存在或已过期| E[下载应用资源]
E --> F[验证资源完整性]
F --> G[启动应用程序]
-
用户通过点击网页上的链接或运行
.jnlp文件来访问 Java 应用程序。 -
Java Web Start 下载
.jnlp文件,该文件包含了应用程序的配置信息,如应用程序的名称、版本、所需的 JAR 文件等。 - Java Web Start 检查本地缓存中是否已经存在所需的 JAR 文件,如果存在且未过期,则直接从缓存中加载;否则,下载所需的 JAR 文件。
- 验证下载的资源的完整性,确保文件没有被篡改。
- 启动 Java 应用程序。
2.4.2 示例
.jnlp
文件
<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec="1.0+" codebase="http://example.com/app/" href="app.jnlp">
<information>
<title>My Java Application</title>
<vendor>My Company</vendor>
<description>An example Java application</description>
</information>
<resources>
<j2se version="1.8+" />
<jar href="app.jar" main="true" />
</resources>
<application-desc main-class="com.example.Main" />
</jnlp>
2.5 JAR 包装与签名
2.5.1 JAR 包装
JAR(Java Archive)文件是一种用于打包 Java 类文件、资源文件和元数据的文件格式。它可以将多个文件组合成一个单一的文件,方便应用程序的分发和部署。
使用
jar
命令可以创建和管理 JAR 文件,以下是一些常见的操作:
- 创建 JAR 文件:
jar cvf myapp.jar com/example/*.class
- 查看 JAR 文件内容:
jar tf myapp.jar
- 提取 JAR 文件内容:
jar xvf myapp.jar
2.5.2 JAR 签名
JAR 签名用于验证 JAR 文件的完整性和来源。签名后的 JAR 文件可以确保文件在传输过程中没有被篡改,并且可以确认文件的发布者。
签名 JAR 文件的步骤如下:
1. 创建密钥库:
keytool -genkeypair -alias mykey -keyalg RSA -keystore mykeystore.jks
- 签名 JAR 文件:
jarsigner -keystore mykeystore.jks myapp.jar mykey
- 验证 JAR 文件签名:
jarsigner -verify myapp.jar
2.6 构建 WAR 文件
WAR(Web Application Archive)文件是一种专门用于打包 Web 应用程序的 JAR 文件。它包含了 Web 应用程序的所有资源,如 JSP 文件、Servlet 类、HTML 文件、CSS 文件等。
2.6.1 WAR 文件结构
一个典型的 WAR 文件结构如下:
myapp.war
├── WEB-INF
│ ├── web.xml
│ ├── classes
│ │ └── com
│ │ └── example
│ │ └── MyServlet.class
│ └── lib
│ └── mylibrary.jar
├── index.jsp
├── css
│ └── style.css
└── images
└── logo.png
2.6.2 构建 WAR 文件
可以使用
jar
命令或构建工具(如 Maven 或 Gradle)来构建 WAR 文件。以下是使用
jar
命令构建 WAR 文件的示例:
jar cvf myapp.war WEB-INF index.jsp css images
2.7 总结
Java 应用的部署涉及到多个方面,包括类路径管理、Java Web Start、JAR 包装与签名、WAR 文件构建等。合理使用这些技术可以提高应用程序的部署效率和安全性。
- 类路径管理 :通过使用相对路径、合并 JAR 文件和使用扩展目录等方法,可以有效管理长类路径,避免类路径带来的问题。
- Java Web Start :提供了一种方便的方式来部署和更新 Java 应用程序,用户可以通过网络直接启动应用。
- JAR 包装与签名 :JAR 文件可以将多个文件组合成一个单一的文件,方便分发和部署;签名可以确保文件的完整性和来源。
- WAR 文件构建 :WAR 文件用于打包 Web 应用程序,包含了 Web 应用的所有资源。
在实际开发中,需要根据应用程序的需求和特点,选择合适的部署方式和技术,以确保应用程序的安全、稳定和高效运行。
超级会员免费看
5万+

被折叠的 条评论
为什么被折叠?



