JAAS

    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()方法。
java

package sample;

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.LoginException;
import javax.security.auth.spi.LoginModule;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 *
 * @author worldheart
 *
 */

public class ScreenContentLoginModule implements LoginModule {
    
    protected static final Log log = LogFactory.getLog(ScreenContentLoginModule.class);
    
    private Subject subject;
    private UsernamePasswordPrincipal principal;
    private CallbackHandler callbackhandler;
    
    public ScreenContentLoginModule() {
        log.info("ScreenContentLoginModule()................");
    }
//在初始化LoginModel后,LoginContext会调用这一方法,从而完成当前LoginModel的初始化工做
    public void initialize(Subject subject, CallbackHandler
            callbackhandler, Map state, Map options) {
        log.info("进入initialize()................");
        this.principal = null;
        this.subject = subject;
        this.callbackhandler = callbackhandler;
    }
//用户阶段1,并认证subject的方法,它可能会收集用户的凭证,好比用户名,密码,并将认证结果存储到LoginModel的实例中
    public boolean login() throws LoginException {
        log.info("进入login()................");
        
        Callback callbacks[] = new Callback[2];
        callbacks[0] = new NameCallback("您的登陆名:");
        callbacks[1] = new PasswordCallback("您的密码:", false);
        
        String username = null;
        String password = null;
        
        try {
            this.callbackhandler.handle(callbacks);
            username = ((NameCallback) callbacks[0]).getName();
            password = new String(((PasswordCallback) callbacks[1]).getPassword());
        } catch (java.io.IOException ioe) {
            throw new LoginException(ioe.toString());
        } catch (UnsupportedCallbackException ce) {
            throw new LoginException(ce.getCallback().toString());
        }
        
        if (username.equals("marissa") || username.equals("scott")) {
            this.principal = new UsernamePasswordPrincipal(username,password);
            return true;
        } else {
            return false;
        }        
    }
//代表认证操做成功,会得到阶段1(login())的认证结果,并将这一结果填充到subject中
    public boolean commit() throws LoginException {
        log.info("进入commit()................");
        if(this.principal == null)
            return false;
        this.subject.getPrincipals().add(this.principal);
        return true;
    }
//认证操做失败
    public boolean abort() throws LoginException {
        log.info("进入abort()................");
        if(this.principal == null)
            return false;
        this.principal = null;
        return true;
    }
//完成subject的销毁工做
    public boolean logout() throws LoginException {
        log.info("进入logout()................");
        if(this.principal == null)
            return false;
        this.subject.getPrincipals().remove(principal);
        this.principal = null;
        return true;
    }
    
}
算法

下面是UsernamePasswordCallbackhandler类:apache

package sample;

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;    }}