Java平台是创建企业应用程序的普遍选择。它所以受欢迎,主要原因之一是在创建Java语言时充分考虑了安全性,而且市场上也普遍认为Java是一种"安全的"语言。Java平台在两个层次上提供安全性:语言层次安全性和企业层次安全性。
1.语言层次安全性
最初的Java(JDK1.2)平台采用沙箱安全模型,基本安全模型由三部分来承担,这三部分构成Java运行环境的三个安全组件,分别是:类加载器,文件校验器,安全管理器
1.1类加载器
类加载器负责从特定位置加载类,类加载器是JVM的看门人,控制着哪些代码被加载或被拒绝,类加载器首先进行安全性检查,它往往从以下几个方面去检查,装入的字节码是否生成指针,装入的字节码是否违反访问限制,装入的字节码是否把对象当作它们本身来访问,它在确保执行代码的安全性方面至关重要。
1.2类文件校验器的校验
类文件校验器负责检查那些无法执行的明显有破坏性的操作,类文件校验器执行的一些检查通常有:变量要在使用之前进行初始化;方法调用和对象引用类型之间要匹配;没有违反访问私有数据和方法的规则;对本地变量的访问都在运行时堆栈内;运行时堆栈是否溢出.如果以上检查中任何一条没有通过,就认为该类遭到了破坏,不被加载。
1.3安全管理器
一旦某个类被类加载器加载到虚拟机中,并由类文件校验器检查过之后,JAVA的第三种安全机制安全管理器就会启动,安全管理器是一个负责控制某个操作是否允许执行的类,安全管理器负责检查包括以下几个方面的操作:当前线程是否能创建一个新的类加载器;当前线程是否能终止JVM的运行;某个类是否能访问另一个类的成员;当前线程是否能访问本地文件;当前线程是否能打开到达外部记住的socket连接;某个类是否能启动打印作业;某个类是否能访问系统剪贴板;某个类是否能访问AWT事件队列;当前线程是否可被信任以打开一顶层窗口。
尽管Java安全的支柱类加载器、类文件校验器、安全管理器每一个都有独特的功能,但它们又相互依赖、相辅相承。共同保证了Java语言的安全性。
2.企业层次的安全特性
即是构建安全的J2EE应用。Java平台在提供语言安全性的同时还提供其他API功能,为企业应用程序提供一个总体的安全性解决方案。下面将介绍几种方案。
2.1 Java加密扩展(JCE)
JCE是一组包,为加密、密钥生成、密钥协商和消息身份验证代码(MAC)算法提供一种框架和实现。JCE支持多种类型的加密,包括对称的、非对称的、块和流密码。在JDK 1.4之前,JCE是一个可选的包,现在它已经成为Java平台的一个标准组成部分。
2.2 Java安全套接字扩展(JSSE)
JSSE是支持安全的Internet通信的一组包。它是实现了SSL和传输层安全(TLS)协议的JAVA技术。它包括用于数据加密、服务器身份验证、消息完整性和可选的客户端身份验证的诸多功能。JSSE已被集成到JDK 1.4以上版本的平台中。
2.3 Java身份验证和授权规范(JAAS)
JAAS通过对运行程序的用户的进行验证,从而达到保护系统的目的。JAAS主要由两个部件构成,认证和授权,JAAS通过一个配置文件来定义认证机制。认证模块是基于可插的认证模块而设计的,它可以运行在客户端和服务器端,授权模块的设计是一个变化的过程,为了对资源的访问请求进行授权,首先需要应用程序认证请求的资源,subject术语来表示请求的资源,用java.security.auth.Subject类来表示subject。subject一旦通过了认证,就会和身份和主体想关联。在JAAS中将主体表示为javax.security.Principal对象,一个subject可能包含多个主题,除了和主题相关联外,subject还可能拥有与安全相关的属性或证书,证书是用户的数据,它包含这样的认证信息即认证subject所拥有的其他服务的信息。基于J2EE的分布式应用程序使用
JAAS一般有两种情况:第一种情况,一个单独的应用系统与一个远程的EJB系统连接,用户必须向应用系统提供证明身份的信息或应用系统向文件和其它的系统来检索可证明身份的信息。这个单独的应用系统将在调用EJB组件之前使用JAAS来验证用户,由应用服务器完成验证的任务。只有当用户通过JAAS的验证之后,客户端程序才可被信任地调用EJB方法。第二种情况,基于Web浏览器的客户端程序连接到Servlet/JSP层,客户端用户将向Servlet/JSP层提供证明身份的信息,而Servlet/JSP层可以采用JAAS验证用户。Web客户端一般可以采
用基本验证、基于表格的验证、摘要验证、证书验证等方式来提供证明身份的信息。这种支持选择不同认证方法的灵活性有助于支持在管理员层实施更为复杂的安全策略,而不是在编程层上去实现。一旦客户端通过应用服务器认证,安全上下文环境能被传播到EJB层。在应用程序中使用JAAS验证通常会涉及到以下几个步骤:
1.创建一个LoginContext的实例。并传递LoginModule配置
文件程序段和CallbackHandler的名称。
2.为了能够获得和处理验证信息,将一个CallbackHandler
对象作为参数传送给LoginContext。
3.通过调用LoginContext的login()方法来进行验证。
4.通过使用login()方法返回的Subject对象实现一些特殊
的功能(假设登录成功)。
举个例子:
LoginModel是jaas的一个核心接口,她负责实施用户认证。同时暴漏了initialize(),login(),commit(),abort(),logout()方法。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; 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 org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author worldheart * */ public class UsernamePasswordCallbackHandler implements CallbackHandler { protected static final Log log = LogFactory.getLog(UsernamePasswordCallbackHandler.class); public void handle(Callback callbacks[]) throws IOException, UnsupportedCallbackException { log.info("进入handle()........................"); for (Callback cb: callbacks) { if (cb instanceof NameCallback) { NameCallback nc = (NameCallback) cb; log.info(nc.getPrompt()); //采集用户名 String username = (new BufferedReader(new InputStreamReader( System.in))).readLine(); nc.setName(username); } else if(cb instanceof PasswordCallback){ PasswordCallback pc = (PasswordCallback) cb; log.info(pc.getPrompt()); //采集用户密码 String password = (new BufferedReader(new InputStreamReader( System.in))).readLine(); pc.setPassword(password.toCharArray()); } } } } 一旦用户收集到用户账号后NameCallback,PasswordCallback对象都会存储他们,与此同时,上述login()方法会基于账号信构建UsernamePasswordPrincipal对象,并保留在登录模块中,而且login()会返回true,当login方法顺利完成用户凭证信息的收集工作后,commit会被触发,她将UsernamePasswordPrincipal对象摆到Subject对象中。 当login方法未能顺利完成用户凭证信息的收集工作后,abort会被触发,将principal等信息破换掉。当登录用户完满的完成自身的业务操作后便可以考虑退出当前的应用,调用logout方法。下面是Principal对象: package sample; import java.security.Principal; /** * * @author worldheart * */ //Acegi中的Authentication接口继承了Principal接口 public class UsernamePasswordPrincipal implements Principal { private String username; private String password; //存储用户名、密码,比如marissa/koala public UsernamePasswordPrincipal(String username, String password) { this.username = username; this.password = password; } public String getName() { return this.username; } public String toString() { return this.username + "->" + this.password; } } 为了使用上述登录模块,需要准备一个jaas配置文件: Loginmodel.conf放在src下面 ScreenContent { sample.ScreenContentLoginModule required; }; 客户应用: package sample; import java.io.File; import java.security.PrivilegedAction; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author worldheart * */ public class JaasSecurityClient { protected static final Log log = LogFactory .getLog(JaasSecurityClient.class); public static void main(String argv[]) throws LoginException, SecurityException { LoginContext ctx = null; ctx = new LoginContext("ScreenContent", new UsernamePasswordCallbackHandler()); //marissa用户登录到当前应用中 ctx.login(); log.info("当前用户已经通过用户认证"); Subject subject = ctx.getSubject(); log.info(subject); // log.info("启用JAAS用户授权能力"); // log.info("临时目录为," + Subject.doAsPrivileged(subject, new PrivilegedAction() { // public Object run() { // log.info("当前用户正在经过JAAS授权操作的考验,并正调用目标业务操作"); // new File("D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf").exists(); // return System.getProperty("java.io.tmpdir"); // } // }, null)); // 退出当前已登录marissa用户 ctx.logout(); } } 在运行客户应用之前还需要提供JVM参数,即引用到loginmoudel.conf配置文件: -Djava.security.auth.login.config=src/loginmoudel.conf 或者通过javahome/jre/lib/security目录中的java.security配置文件指定上述loginmoudel.conf配置文件: #login.config.url.l=file:${user.home}/.java.login.config login.config.url.l=file:d:/eclipse/src/loginmoudel.conf SecurityContextLoginModule是Acegi内置的一个LoginModel实现,当开发Jaas应用时,用户凭证信息的获取可能来自Acegi,此时,我们便可以采用内置的SecurityContextLoginModel。要使用SecurityContextLoginModule,我们需要在Jaas配置文件中配置它: ACEGI { org.acegisecurity.providers.jaas.SecurityContextLoginModule required ignoreMissingAuthentication=true; }; 客户端应用: package sample; import java.io.File; import java.security.PrivilegedAction; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author worldheart * */ public class AcegiSecurityClient { protected static final Log log = LogFactory .getLog(AcegiSecurityClient.class); public static void main(String argv[]) throws LoginException, SecurityException { LoginContext ctx = null; //在实际企业应用中,Authentication对象的构建形式多种多样 SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken("marissa", "koala")); ctx = new LoginContext("ACEGI"); // marissa用户登录到当前应用中 ctx.login(); log.info("当前用户已经通过用户认证"); Subject subject = ctx.getSubject(); log.info(subject); // log.info("启用JAAS用户授权能力"); // log.info("临时目录为," // + Subject.doAsPrivileged(subject, new PrivilegedAction() { // public Object run() { // log.info("当前用户正在经过JAAS授权操作的考验,并正调用目标业务操作"); // new File( // "D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf") // .exists(); // return System.getProperty("java.io.tmpdir"); // } // }, null)); // 退出当前已登录marissa用户 ctx.logout(); //清除已注册的SecurityContext SecurityContextHolder.clearContext(); } } 注意到我们并未为LoginContext提供CallbackHandler对象,由于Acegi负责提供兼容于Principal的Authentication对象,因此用户凭证的收集也不用CallbackHandler操心了。 在运行客户应用之前还需要提供JVM参数,即引用到loginmoudel.conf配置文件: -Djava.security.auth.login.config=src/loginmoudel.conf 或者通过javahome/jre/lib/security目录中的java.security配置文件指定上述loginmoudel.conf配置文件: #login.config.url.l=file:${user.home}/.java.login.config login.config.url.l=file:d:/eclipse/src/loginmoudel.conf 启用Java安全管理器:大部分java开发者都知道,借助如下JVM参数能够启用java安全管理器,-Djava.security.manager。既然如此,我们通过如下JVM参数运行JaasSecurityClient客户端和AcegiSecurityClient客户端: -Djava.security.manager -Djava.security.auth.login.config=src/loginmodule.conf 但是这样会出错:java.security.auth.login.config.AccessControlException:access denied 出错原因:默认时,直接借助“-Djava.security.manager”启动java安全管理器,JVM会采用javahome/jre/lib/security中的java.policy策略文件,而这一策略文件并未对上述涉及到的各种权限(比如:createLoginContext.ScreenContent,读取acegi.security.strategyJava属性)进行授权因此抛出了异常。 为此我们可以提供新的授权信息jaassecuritypolicy.txt策略文件。由于我们需要同LoginContext进行各类操作因此需要提供相关AuthPermission权限给Acegi SecurityClient,同时我们使用了Commons-Logging,Log4j管理日志,因此还必须将相应的操作权限给这一客户,在操作日志的过程中,客户应用需要操控的:d:/ddlog.log日志文件因此需要将读写权限授给这一客户应用。 grant codebase "file:./-"{ permission java.io.FilePermission "D:/contactsforchapter8.log", "read, write"; permission javax.security.auth.AuthPermission "createLoginContext"; permission javax.security.auth.AuthPermission "modifyPrincipals"; }; grant codeBase "file:D:/eclipse/workspace/contactsforchapter8/context/WEB-INF/lib/log4j-1.2.14.jar" { permission java.security.AllPermission; }; grant codeBase "file:D:/eclipse/workspace/contactsforchapter8/context/WEB-INF/lib/commons-logging-1.0.4.jar" { permission java.security.AllPermission; }; 实际上java的策略文件编写可以通过policytool工具。 运行JaasSecurityClient客户端应用: -Djava.security.manager -Djava.security.policy=src/jaassecuriypolicy.txt -Djava.security.auth.login.config=src/loginmodule.conf 类似的运行AcegiSecurityClient的策略文件: grant codebase "file:./-"{ permission java.util.PropertyPermission "acegi.security.strategy", "read"; permission java.io.FilePermission "D:/contactsforchapter8.log", "read, write"; permission javax.security.auth.AuthPermission "createLoginContext"; permission javax.security.auth.AuthPermission "modifyPrincipals"; }; grant codeBase "file:D:/eclipse/workspace/contactsforchapter8/context/WEB-INF/lib/log4j-1.2.14.jar" { permission java.security.AllPermission; }; grant codeBase "file:D:/eclipse/workspace/contactsforchapter8/context/WEB-INF/lib/commons-logging-1.0.4.jar" { permission java.security.AllPermission; }; 运行AcegiSecurityClient客户端应用: -Djava.security.manager -Djava.security.policy=src/acegisecuriypolicy.txt -Djava.security.auth.login.config=src/loginmodule.conf 启用Jaas的用户授权功能:jaas的授权能力依赖java策略文件,下面提供了另一个版本的jaasSecurityClient客户应用,新增了两行java代码: LoginContext ctx = null; ctx = new LoginContext("ScreenContent", new UsernamePasswordCallbackHandler()); //marissa用户登录到当前应用中 ctx.login(); log.info("当前用户已经通过用户认证"); Subject subject = ctx.getSubject(); log.info(subject); new File("D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf").exists(); System.getProperty("java.io.tmpdir"); // 退出当前已登录marissa用户 ctx.logout(); 此时开发者必须往jaassecuritypolicy.txt策略文件中添加如下权限到其中: permission java.io.FilePermission "D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf", "read"; permission java.util.PropertyPermission "java.io.tmpdir", "READ"; 如果客户要求只具有marissa用户才有权利运行上述两行代码,那么应该这样: LoginContext ctx = null; ctx = new LoginContext("ScreenContent", new UsernamePasswordCallbackHandler()); //marissa用户登录到当前应用中 ctx.login(); log.info("当前用户已经通过用户认证"); Subject subject = ctx.getSubject(); log.info(subject); // log.info("启用JAAS用户授权能力"); // log.info("临时目录为," + Subject.doAsPrivileged(subject, new PrivilegedAction() { // public Object run() { // log.info("当前用户正在经过JAAS授权操作的考验,并正调用目标业务操作"); // new File("D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf").exists(); // return System.getProperty("java.io.tmpdir"); // } // }, null)); // 退出当前已登录marissa用户 ctx.logout(); 那么jaassecuritypolicy.txt策略文件应该添加如下内容: grant codebase "file:./-", Principal sample.UsernamePasswordPrincipal "marissa" { permission java.io.FilePermission "D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf", "read"; permission java.util.PropertyPermission "java.io.tmpdir", "READ"; }; 启动jaassecurityclient客户端: -Djava.security.manager -Djava.security.policy=src/jaassecuriypolicy.txt -Djava.security.auth.login.config=src/loginmodule.conf 那么对于acegisecurityclient客户应用,acegisecuritypolicy.txt应该增加: grant codebase "file:./-", Principal org.acegisecurity.providers.UsernamePasswordAuthenticationToken "marissa" { permission java.io.FilePermission "D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf", "read"; permission java.util.PropertyPermission "java.io.tmpdir", "read"; }; 启动: -Djava.security.manager -Djava.security.policy=src/acegisecuriypolicy.txt -Djava.security.auth.login.config=src/loginmodule.conf 直击JaasAuthenticationProvider 配置: <bean id="jaasAuthenticationProvider" class="org.acegisecurity.providers.jaas.JaasAuthenticationProvider"> <property name="authorityGranters" <bean class="sample.TestAuthorityGranter"/> </property> <property name="callbackHandlers" <list> <bean class="org.acegisecurity.providers.jaas.JaasNameCallbackHandler"/> <bean class="org.acegisecurity.providers.jaas.JaasPasswordCallbackHandler"/> </property> <property name="loginConfig" value="classpath:acegi.conf"/> <property name="liginContextName" value="ACEGI"/> </bean> 另外需要将JaasAuthenticationProvider添加到认证管理器: acegi.conf的内容: ACEGI { sample.TestLoginModule required; }; 注释:authorityGranters属性能够为已经认证用户提供角色映射信息,由于这里的Jaas仅负责用户认证,而授权仍然被acegi接管。TestAuthorityGranter实现类: package sample; import java.security.Principal; import java.util.HashSet; import java.util.Set; import org.acegisecurity.providers.jaas.AuthorityGranter; /** * * @author worldheart * */ public class TestAuthorityGranter implements AuthorityGranter { public Set grant(Principal principal) { Set<String> rtnSet = new HashSet<String>(); if (principal.getName().equals("TEST_PRINCIPAL")) { rtnSet.add("ROLE_USER"); rtnSet.add("ROLE_ADMIN"); } return rtnSet; } } 下面是TestLoginModel类: package sample; import java.security.Principal; 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.login.LoginException; import javax.security.auth.spi.LoginModule; /** * * @author worldheart * */ public class TestLoginModule implements LoginModule { private String user; private String password; private Subject subject; public boolean abort() throws LoginException { return true; } public boolean commit() throws LoginException { return true; } public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { this.subject = subject; try { NameCallback nameCallback = new NameCallback("prompt"); PasswordCallback passwordCallback = new PasswordCallback("prompt", false); callbackHandler.handle(new Callback[] {nameCallback, passwordCallback }); user = nameCallback.getName(); password = new String(passwordCallback.getPassword()); } catch (Exception e) { throw new RuntimeException(e); } } public boolean login() throws LoginException { if (!user.equals("marissa")) { throw new LoginException("用户名不对"); } if (!password.equals("koala")) { throw new LoginException("密码不对"); } subject.getPrincipals().add(new Principal() { public String getName() { return "TEST_PRINCIPAL"; } }); subject.getPrincipals().add(new Principal() { public String getName() { return "NULL_PRINCIPAL"; } }); return true; } public boolean logout() throws LoginException { return true; } } |