JBoss下利用JAAS处理登录 - 刘学文的专栏 - CSDNBlog

来源:百度文库 编辑:神马文学网 时间:2024/05/02 06:50:45
  在J2EE项目中,登录模块大致有两种方式来实现:一是请求每个View(包括jsp,servlet等)的时候通过过滤器来检查权限,这个我就不多说了。另外一种方式就是采用应用服务器集成的JAAS技术来处理登录,具体的实现方式可能在不同的应用服务器上略有不同,但原理基本一样。在这里我主要介绍一下JBoss下的JAAS登录模块的编写和配置。

       两年前参与一个项目的时候,第一次接触到JAAS技术,那个项目采用IBM的Websphere做应用服务器。感觉非常的简洁和严谨:没用登录的用户在请求任意一个设置了权限的资源时都会转向登录页面(在这个登录页面的源代码里你绝对看不到隐藏了要跳转的页面的URL),登录完成后再转到请求的资源,如果用过滤器来实现这种登录验证方式可能要写很多的代码,效果也未必很好,用JAAS的方式来处理则非常简单,配置一下就好了,甚至不用写代码。

  于是等到去年自己负责一个项目的时候也采用了这种安全验证方式,由于这方面的技术资料很少,所以也摸索了不少时间。一开始是用Tomcat做Web服务器,后来迁移到了Jboss平台,我在这里主要介绍一下Jboss下这方面的技术,如果还有精力也提一下Tomcat下的实现方式。先来罗列一下接下来我要介绍的内容:

  1. JAAS概述,Jboss下配置安全性验证的几种方式
  2. 从数据库里读取登录用户的验证信息
  3. 用自己的类来实现JAAS登录模块
 1.JAAS概述,Jboss下配置安全性验证的几种方式 关于JAAS概念性的东西我就不自己写了,主要内容大部分来自Sun的官方资料《java_security2》。关于Jboss的安全性配置的基本内容也来自Jboss的资料,所以这部分的资料完全都是转贴,请不要见怪,因为在介绍更高级的应用之前必须先介绍这方面的内容。
  • Java 鉴别与授权服务(JAAS)
    JAAS(Java Authentication and Authorization Service)可以在Java 平台上提供用户身份鉴别。在Java 平台上,其功能是独一无二的。
    Java 安全设计的所有核心功能都是为了保护终端用户不受程序开发者的影响,由于终端用户为了使用开发者提供的程序,不得不满足其对本地资源的存取要求,这就要求必须保证用户不受到程序的影响。另一方面,JAAS 允许开发者根据用户提供的鉴别信任状(credential)准许(或拒绝)用户对程序的访问。
    JAAS 可由 http://java.sun.com/products/jaas/ 下载。它包括两部分: 一部分是Java类库,其中定义了服务接口(JAAS 固有的),另一部分为平台专用的模块,其作用是实现授权(JAAS 模块)。此外还可以获得模块样例,从而在JNDI 目录服务、Windows NT 登录服务和Solaris 登录服务基础上实现鉴别。
    JAAS 本身包括文档和一个lib 目录,其中只有一个jar 文件(jaas.jar)。可以将此jar 文件安装到$JREHOME/lib/ext 中,用户也可以将它放在类路径指定的目录下。JAAS模块的lib 目录中包括另一个jar 文件(jaasmod.jar),对其处理方法与jaas.jar类似。

  • Jboss安全性配置

    J2EE定义了应用中指定安全性约束的机制,但并没有给出具体服务器是如何实现和配置认证和访问控制机制(译者注:授权)的。JBoss使用了JAAS以提供集成不同认证技术的可插入方式,供应用的认证和授权使用。同时,JBoss还提供了一套标准模块,即基于文件、数据库、LDAP的安全性机制。其中,基于文件的方式最为简单。用户可以使用现有的认证模块,或者可以开发更适合自身需求的认证模块。
    Jboss安全性域信息存储在login-config.xml文件中,其包含了许多安全性域定义。各个安全性域指定了许多JAAS3登陆模块,供安全性域认证使用。当用户需要在应用中使用安全性时,需要在JBoss特定部署描述符jboss.xml或jboss-web.xml中指定待使用的安全性域名。本节将快速地带领用户分析如何保护随JBoss发布的JMX控制台和Web控制台应用。
    通过JMX控制台基本上能够控制JBoss服务器的各个方面,因此保护该控制台很重要,至少需要通过密码来保护它。否则,任何远程用户将能够完全控制用户的JBoss服务器。为实现此目的,本文将安全性域添加给JMX控制台应用。通过server/default/deploy/jmx-console.war/WEB-INF/目录能够找到待修改的JMX控制台文件,即jboss-web.xml。将jboss-web.xml中security-domain的注释去掉,具体如下。

    java:/jaas/jmx-console

    这将设置Web应用待使用的安全性域,但还未确定Web应用应该使用的安全性策略。待保护的URL是什么,相应的访问角色又是哪些?为配置这些内容,用户需要在同一目录中找到web.xml文件,然后将security-constraint的注释去掉。其中,该安全性约束要求登陆用户必须具有JBossAdmin角色。



    HtmlAdaptor

    An example security config that only allows users with the
    role JBossAdmin to access the HTML JMX console web application

    /*
    GET
    POST


    JBossAdmin


    太好了,但是用户名和密码来自哪里呢?不错,它们来自jmx-console安全性域。通过conf/login-config.xml文件能够看到。


    flag="required">

    jmx-console-users.properties


    jmx-console-roles.properties




    上述配置使用了基于简单文件的安全性策略。其中,登陆JMX控制台应用的用户名和密码存储在jmx-console-users.properties中,并且以“username=password”形式给出。为了将用户添加到JBossAdmin角色中,需要将“username=rolename”形式给出的用户和角色映射关系给出在jmx-console-roles.properties文件中。现存的文件创建了admin用户,其密码为admin。用户可能删除该用户,或者更改其密码,使得JMX控制台应用更安全。
    当更新了web.xml时,JBoss会重新部署JMX控制台应用。用户可以通过服务器控制台检查JBoss保存了对web.xml所作的修改。如果用户正确地配置了上述各个任务,并且重新部署了JMX控制台应用,则下次访问它时,JBoss会要求用户提供用户名和密码。
    JMX控制台应用不是JBoss唯一提供的、基于Web的管理界面。JBoss还提供了另一管理应用,即Web控制台(参考附录A)。尽管Web控制台是基于Java Applet形式给出的,但是对应的Web应用还是可以类似于JMX控制台的方式来保护它。其中,Web控制台应用位于default/deploy/management/web-console.war。注意,这同JMX控制器应用有所区别,因为JMX控制器应用是以目录形式展开的。因此,编辑web-console.war WAR存档更费力些。

  • 配置Jboss安全性域
    通常,用于Web和EJB层的、标准的J2EE安全性声明需要借助于web.xml和ejb-jar.xml部署描述符指定。然而,为配置使用JBoss安全性,开发者还需要提供JBoss专有的部署描述符。
    通过JBoss具体部署描述符能够完成应用的安全性配置。为保护Web应用,用户需要将security-domain元素包含在jboss-web.xml中。

    java:/jaas/dukesbank
    ...

    如果需要在EJB层实现访问控制,则用户也可以对jboss.xml文件添加同样的security-domain元素。

    java:/jaas/dukesbank

    ...


    这将意味着,JBoss会在JNDI名“java:/jaas/dukesbank”下为Duke银行应用绑定安全性管理器实例。所有的安全性域配置在java:/jaas上下文,因此Duke银行应用实际上使用了dukesbank安全性域。
    用户通过conf/login-config.xml文件能够配置它。但是,如果浏览Duke银行应用,并不能够找到dukesbank安全性域。一旦JBoss寻找不到相应的安全性域,则它会使用other域。其中,other域的具体配置如下。


    flag="required" />


    此处,登陆模块使用本地属性文件来认证应用。JBoss借助于两个文件进行认证工作。其一,提供用户名、密码;其二,提供角色。比如,如下是Duke银行应用的users.properties。
    # users.properties file for use with the UsersRolesLoginModule
    # Format is:
    #
    # username=password
    200=j2ee
    属性文件的格式非常简单。各行采用username=password的形式。因此,上述文件定义了200用户,其密码为j2ee。这就是用户访问Duke银行应用的帐号。如果用户修改了上述密码,则需要重新部署Web应用。
    当然,用户名和密码不是驱动J2EE应用安全性的唯一因素。部署者需要将用户指定角色,因此应用会根据用户的角色信息来决定用户是否有权访问目标资源。只有是Duke银行应用的客户才有权访问它,即通过bankCustomer角色。下面给出了roles.properties文件,用于指定200用户的角色。
    # A roles.properties file for use with the UsersRolesLoginModule
    #
    # Format is
    #
    # username=role1,role2,role3
    200=bankCustomer

    为让Duke银行应用使用dukesbank安全性域,而不是使用服务器提供的默认安全性域,开发者还需定义出dukesbank安全性域。因此,开发者需要往conf/login-config.xml文件中添加如下内容:


    flag="required" />


    注意,需要重启JBoss,否则修改后的login-config.xml不会生效。

  •  使用RDBMS实现安全性
    实际应用中,将用户、角色信息存储在数据库中很常见。JBoss提供了称之为DatabaseServerLoginModule的登陆模块,用户只需要做少许的配置即可使用它。用户需要提供如下内容:
    ?? 获得用户密码的SQL查询语句
    ?? 获得用户角色的SQL查询语句
    ?? 待使用数据源的JNDI名
    因此,可以使用现有的数据库模式。现假定:使用如下SQL语句创建安全性相关的数据库表。
    CREATE TABLE Users(username VARCHAR(64) PRIMARY KEY, passwd VARCHAR(64))
    CREATE TABLE UserRoles(username VARCHAR(64), userRoles VARCHAR(32))
    INSERT INTO Users VALUES ('200','j2ee')
    INSERT INTO UserRoles VALUES ('200','bankCustomer')
    然后,将它们作为Duke银行应用的安全性数据库。需要对login-config.xml作如下修改:


    flag="required">
    java:/DefaultDS

    select passwd from Users where username=?


    select userRoles,'Roles' from UserRoles where username=?




    其中,获得密码的SQL语句很好理解。但是,对于获得角色的SQL语句而言,多出了称之为“Roles”的角色组域。用户可以添加自身需要的、处于该角色组中的其他角色。JBoss本身要求,其取值是“Roles”。本实例的数据库中,仅仅存在一套角色,并无角色组信息5。
    上述登陆模块中,使用了默认数据源。如果用户在使用Hypersonic,则可以使用数据库管理器工具创建数据库表、添加数据(Duke银行应用一章曾给出介绍)。

  • 使用密码散列
    到目前为止,本书使用到的登陆模块都支持密码散列,即不是以明文存储密码,而是存储单工密码(比如,使用MD5)。这同UNIX系统中的/etc/passwd文件类似。因此,用户即使浏览到密码,也不能够登陆到系统中。然而,这种方式也有缺点。其一,万一用户忘记密码,则不能够恢复。其二,管理工作变得较复杂,因为用户需要计算密码散列,并放置在安全性数据库中。当然,这并不妨碍其使用。为生效密码散列,用户需要添加如下模块选项。
    MD5
    base64
    这表明使用了MD5散列、base64编码,从而将二进制散列值转换为字符串。JBoss在认证用户之前,会使用上述选项计算出密码的散列,因此用户必须确保数据库中存储了正确的散列信息。如果用户使用UNIX系统,或者在Windows上安装了Cygwin,则可以使用openssl计算散列。
    $ echo -n "j2ee" | openssl dgst -md5 -binary | openssl base64
    glcikLhvxq1BwPBZN0EGMQ==
    用户应该将“glcikLhvxq1BwPBZN0EGMQ==”插入到数据库中,而不是明文,“j2ee”。或者,可以使用jbosssx.jar中的org.jboss.security.Base64Encoder类。
    $ java -classpath ./jbosssx.jar org.jboss.security.Base64Encoder j2ee MD5
    [glcikLhvxq1BwPBZN0EGMQ==]
    如果用户只提供单个参数,则上述类只会以base64对它进行编码。但如果将加密算法名作为第二个参数,则它将计算出第一个参数的散列。

2.从数据库里读取登录用户的验证信息     在我的项目一开始的时候由于时间关系,没能摸清楚如何实现用自己写的模块处理登录,所以就采用了比较简单RDBMS(即用Jboss的DatabaseServerLoginModule)模块处理登录事件,然后通过一个过滤器判断用户是否已经登录,如果登录了再从数据库中读取用户信息到session中。虽然自己都感觉到有点不伦不类,但我还是将我的具体实现方式在下面写出来: 1.     配置login-config.xml编辑Jboss主目录下的 server/default/conf 目录下的login-config.xml文件,在节点下加入如下的内容: ….                                                      java:/ testds                                    SELECT PASSWORD FROM OPERATOR WHERE LOGINID=?                                   SELECT ROLENAME,'Roles' FROM V_ROLE WHERE LOGINID=?                                    MD5                                             base64                         …. 在这里顺便简单介绍一下MD5和Base64。MD5是一种不可逆的编码算法,即你不可能由已经加密的密码字符串来根据固定的算法还原出原来的密码,据说现在的银行系统大多数采用这种方式来储存用户密码,所以如果用户的密码遗失了,银行也不可能通过查数据库给你找回你原来设的密码,只能重新设置。要破解MD5的密码只能用很笨很好时的穷举法。       而Base64的编码其实大家都应该经常接触,我们的电子邮件一般都是经过Base64编码后传送的,如果您用的是Outlook Express收发邮件,那你可以通过邮件的“属性/详细信息/邮件来源”看到邮件的Base64编码的信息。它将二进制数据编码为字母和数字,因为字母和数字的个数少于256,所以它是把3个字节的二进制数据转换为4个字节的字母和数字。所以经过base64编码的文件会比原来的文件大4/3倍。Jboss用专门的API来处理md5加密及base64的编码和反编码。       2.       配置连接池       上面的文件中用到了一个名为testds的连接池,现在就来配置这个连接池。我这里用的是oracle数据库,其它的数据库的配置方式就不一一介绍了。具体的表结构和建表的sql语句我这里也不再描述了,如果有不清楚的地方清留言问我。编辑JBoss主目录下的server\default\deploy目录 (什么?你的目录下没有找到这个文件,没关系,从你的Jboss主目录的docs\examples\jca下copy一个过来)。在节点之间加入如下内容:      testds    jdbc:oracle:thin:@127.0.0.1:1521:mydb     oracle.jdbc.driver.OracleDriver    test    123    org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter               Oracle9i         3.       配置jboss-web.xmlJBoss下的配置就完成了,下面再配置我们的项目里的文件。打开你的Web应用下的Web目录下的的WEB-INF目录,新建一个jboss-web.xml的文件,在文件中加入如下内容:     java:/jaas/ nccnm  这个配置会告诉Jboss我们这个应用的JAAS安全性策略到哪里去找。 4.       配置web.xml打开你的Web应用下的Web目录下的的WEB-INF目录,编辑web.xml (这个文件应该能找到吧?如果找不到就麻烦了,从头去啃J2EE),加入如下的内容:            login required      *.jsp      *.htm      *.html      *.do              role1                 FORM    mydomain                 /login.jsp                                  /error.jsp          role1  以上内容的作用是本Web应用下的所有jsp,htm,html,do为扩展名的资源都要登录后才能访问(你可以根据你的需要来设置,通常你可以设置* 来限制没有登录所有的资源都不能访问),而且只有role1角色的用户才能访问。 如果我想要某些jsp页面不登录也能访问怎么办?我是这样处理的,也不知道是不是歪门邪道,再加一个security-constraint策略:           not login required      /report/*        /css/*       这样你的Web应用下的report和css目录下的任何资源都不需要登录就可以访问了。 5.       配置登录页面在Web应用的主目录下新建一个login.jsp文件,加入如下内容:<%@ page contentType="text/html; charset=GBK" %>登录页面
      
 
    
  OK,现在大功告成,把你的Web应用发布到Jboss下试试吧。其实我这里介绍的内容在我的上一篇文章都已经描述了,只不过我是把具体一实现又重新写了一遍。在下一篇文章中我将要介绍一下如何实现自己编写的登录模块,如何把用户信息保存到session中,可能这种方式才是最主要的。 在我心目中理想的登录模块应该是这样的,我可以通过配置文件将一些事交给应用服务器的JAAS去处理,但是有必要时我也能够通过重载某些方法拦截登录处理的信息来达到自己的某些目的,比如记录登录事件、读取登录用户的信息放到session中,甚至是再加上一个处理验证码。这些能够实现吗?在Tomcat中好象不行,但是在Jboss中呢?

       由于一直没有时间去解决在JAAS中集成自己编写的验证处理模块的技术,我上一篇介绍的方法在我的项目中用了半年,等我把其他紧要的事情了结之后又回过头来琢磨登录模块的事了。

       我反反复复看了login-config.xml文件,我对这一段产生了一些想法:

             flag = "required">

       我能不能继续org.jboss.security.auth.spi.DatabaseServerLoginModule,然后把这个code的值替换为我自己编写的类呢?Jboss既然提供了这么灵活的配置,那么说理论上应该是可行的,于是我又拿过来Jboss的源代码,仔细地了解了登录方面的一些接口和类,尤其是这个DatabaseServerLoginModule类更是把每一行代码都反复的研究过,我试着编写了一个com.benjamin.commons.security.LoginModule类,并且重载了login函数和initialize函数,加入了一些测试代码,试验了几次测试结果之后决定从重载login函数着手。这个函数是这样定义的:

public boolean login() throws LoginException ;

       在这个函数里可以通过this.getUsername()来取得登录的用户名,然后根据用户名从数据库读取用户信息也没问题,但是session呢?我怎么取得session对象并往里面加东西?这个类即没有继承servlet也没有任何属性或参数与request有关,看来这个session是没办法获取了?我很不甘心就这样放弃,在google上输入了各种关键字来查找有用的信息,国内介绍JAAS的资料本来就少之又少,更别说这牛角尖一样偏门的方面了,终于网络不负有心人,我在一个国外的论坛上找到了一点资料,原来在Jboss中可以通过PolicyContext这个类来获取应用服务器的安全性方面的上下文,其中就包括获取HttpServletRequest,具体方法是这样的:

 

HttpServletRequest request =

                     (HttpServletRequest)PolicyContext.getContext(WEB_REQUEST_KEY);

 

后来我更发现只要是在Jboss容器运行的任何类都可以通过这种方式获取request对象。

有了request获取session当然就不成问题了:

HttpSession session=request.getSession(true);

最后我的这个login函数变成了这样:

 

    public boolean login() throws LoginException {

       boolean loginAccepted =false;

       HttpServletRequest request = null;

   

       try {

//         System.out.println("开始调用JAAS验证 ...");

           request = (HttpServletRequest)PolicyContext.getContext(WEB_REQUEST_KEY);

           HttpSession session=request.getSession(true);

       //  String loadPath = this.getClass().getProtectionDomain().getCodeSource().getLocation().toString();

          

//         log.info("开始调用JAAS验证 ...");

           loginAccepted = super.login();

           log.info("已通过JAAS验证,开始获取登录用户信息");

       } catch (LoginException e) {

           request.setAttribute("ex", e);

           LoginException ex=new LoginException("用户" + this.getUsername() +"登录失败:" + e.getMessage());

           log.error(e);

           throw ex;

       } catch (PolicyContextException e) {     

           LoginException ex=new LoginException("获取request失败:" + e.getMessage());

           log.error(ex);

           request.setAttribute("ex", ex);

           throw ex;

       }

      

       if(loginAccepted) {        

           try {

               String userName=this.getUsername();

 

              //根据登录用户名读取用户信息保存到session中

                if (loginBo.login(userName,request)) {

                    log.info(userName + " 登录成功.");

                }

                else{

                      log.error(userName + "登录失败!");          

                      throw new LoginException("登录失败,无法从数据库获取用户信息!");

                }                     

           }         

           catch(Exception e) {

              LoginException ex=new LoginException("登录失败:" + e.getMessage());

              log.error(ex);

              request.setAttribute("ex", ex);

               throw ex;

           }      

          

       }

       return loginAccepted;

    }

 

    然后我把login-config.xml的相关配置改成了这个样子:

 

 

      

                  

         

             flag = "required">

             java:/testds

             SELECT PASSWORD FROM REL_PUB_OPERATOR  WHERE LOGINID=?

             SELECT ROLENAME,'Roles' FROM VREL_ROLE WHERE LOGINID=?

             MD5

base64          

      

   

 

    这样我想实现的读取用户信息到session的功能就实现了,但是我想更灵活更通用一点,我在这个项目中用的loginBo的类和其中的login(userName,request)方法很可能在其他项目中就不一样了,我应该也通过配置来实现自定义自己的登录完毕后的处理类。我在前面几次测试的过程中发现 的值是可以在loginModule类的initialize函数里通过options.get(String)方法获取得到,于是我改了改登录配置文件,加了一行:

    com.system.business.LoginBo

 

变成这样:

 

      

                  

         

             flag = "required">

             java:/testds

             SELECT PASSWORD FROM REL_PUB_OPERATOR  WHERE LOGINID=?

             SELECT ROLENAME,'Roles' FROM VREL_ROLE WHERE LOGINID=?

             MD5

                                                         base64

                            com.benjamin.system.business.LoginBo      

                

 

 

然后又在我的LoginModule类里重载了initialize函数:

 

    public void initialize(Subject subject,CallbackHandler callbackHandler,Map sharedState,Map options){

       super.initialize(subject, callbackHandler, sharedState, options);

       if(options.get("loginBo")!=null){

           try {

               loginBo=(ILoginBo)Class.forName(options.get("loginBo").toString()).newInstance();

           } catch (InstantiationException e) {

              log.error(e);

           } catch (IllegalAccessException e) {

              log.error(e);

           } catch (ClassNotFoundException e) {

              log.error("没有找到loginBo类:" + e);

           }

       }

    }

 

这样我就可以很灵活地通过配置实现代码的移植重用,在其它项目里只要重新实现一个loginBo类并修改配置为这个类就可以了。

完成之后重新发布到jboss上,运行Jboss,打开登录页面,登录成功,非常完美!

按理这样就算大功告成了,但是当我再次登录的时候发现登录后转向的页面出了一些错误,session里读出来的值都是null,再试了几次,换另外一个用户名登录又是好的,但退出后再次用这个用户名登录时就不行了。这个问题困扰了我好几天,我感觉应该是Jboss保存了登录用户的信息在缓冲区导致一些方面冲突。但是怎么解决呢?

最后还是在网上找到了解决办法,看来离开网络我是没办法再混下去了。

修改JBOSS安装目录下的server\default\deploy\security-service.xml文件,找到这样的一个节点

 

      name="jboss.security:service=JaasSecurityManager">

60

600

 

 

将DefaultCacheTimeout和DefaultCacheResolution的值都改为0,即不将登录信息保存到缓冲区,其它地方不变,修改完后这两个节点应该是这样的:

 

0

0

 

   重启Jboss,登录,没问题,再次登录也没问题,经过多次测试后发现这一招果然管用,我用的Jboss版本是4.0.3SP1,后来我在布署其它版本的Jboss时发现没有security-service.xml文件,但是我在这个版本的Jboss的主目录下的server\default\conf\jboss-service.xml配置文件里也找到了同样的DefaultCacheTimeout和DefaultCacheResolution配置,照样修改为0,运行以后发现完全正常。

       至此我想实现的功能基本上都可以实现了,我之所以加上一个“基本上”,是因为我前面还提到了一个加验证码的功能,我试过几次没有成功,因为我在登录模块里只能获取到页面提交的用户名和密码信息,我试过加上一个textbox来填写验证码,但是不知道在哪里能取得出来,前面所提的方法里获取的request里是取不到,不知道还有什么其它方式。希望有兴趣的朋友可以自己去试一下,试出来了别忘了告诉我。