基于容器的用户安全管理系统和JMS基础教程实例

来源:百度文库 编辑:神马文学网 时间:2024/04/27 18:24:07

1  需求分析

在很多实际应用中,不只是简单地要求用户需要注册登录。还要求不同的用户对不同资源拥有不同的权限。某单位的新闻部门的某些人才拥有对外发布新闻的权限;每个部门只有对自己部门内部资源才拥有创建、修改权限,对于其他部门创建的资源如网页等只有浏览的权限。这些复杂的用户权限验证需要可以总结为如下几点:

·          用户资料管理系统:对用户的注册资料进行管理。可进行用户资料的新增修改,能够查询获取丢失的密码。

·          权限的验证和授权:拦截所有访问受保护资源的请求,检查该用户是否被授权访问该资源。

·          可控制的资源限制访问:对于系统的资源,无论是数据或行为动作,对确定的一组用户都有限制访问。例如对于有的用户只允许读操作,不能进行写操作;有些资源只有创建者才可以访问和操作等。

这3种需求适合大部分应用系统,通过它们的实现,可以形成一个用户安全管理方面的组件框架,并且能够重复地在不同的应用系统中使用。 

2  架构设计

实现一个完整的用户安全管理框架,可以有两种实现途径。在J2EE出现以前,大部分是由应用系统本身实现。因此,很多有经验的软件商都拥有自己成熟的用户安全管理系统,但是缺点也是比较明显,自己设计的用户安全管理系统可重用性低,有的和具体应用程序过分紧密地绑定在一起,无法移植到其他系统上。

但是,随着业务量上升,应用系统的不断增加,会出现不同应用系统拥有不同的用户登录验证体系,很显然,这给用户访问带来了不方便,用户不可能为每个系统注册一套用户和密码,定制一套用户角色。因此,整个服务器群中需要统一的用户权限验证体系。而J2EE容器的用户安全管理框架再辅助以LDAP或数据库系统,可以很方便地达到这个目标。


2.1  角色

J2EE容器的用户安全框架是基于RBAC(Roled-Based Access Control,相关网址:http://csrc.nist.gov/rbac/)设计模型建立的,这是一个基于角色的访问权限控制模型。

首先必须了解角色的含义,在RBAC中角色Role的定义是:Role是明确表达访问控制(Aceess Control)策略的一种语义构建词。

角色可以是指做某些事情的资格,比如医生或物理学家;也可以包含权力和责任的意思,如部门经理或局长等。角色和组(groups)是有区别的。组就是纯粹代表一群用户;角色一方面代表一系列用户,另外一方面可以代表一系列权限,因此可以说是用户和权限的结合体。

引入角色的概念主要是为了分离用户和访问权限的直接联系。用户与访问权限的直接组合可能是短暂的,而角色则可以相对稳定,因为一个系统中和角色相关的权限变化是有限的。

在RBAC理论出现之前,很多人都是把用户和权限混淆在一起,这样当用户或权限发生变化时,都会涉及到对方,很显然这在实际实现中将是非常复杂的。所以诞生RBAC,创造了一个“角色”的名词,注意这是人为创造的语义词。角色就是用户和权限之间的第3者,通过引入角色概念,将用户和权限的关系解耦。这样用户的变化只要涉及到角色就可以,无需考虑权限。而权限的变化只涉及到角色,无需考虑用户或用户组。

因此,基于角色的访问控制系统可以分为两个部分:与角色相关的访问权限系统以及与角色相关的用户管理系统。这样,通过角色这个中间者,将用户和权限联系在一起。其实这也非常符合日常生活的逻辑,例如王三来到某公司做业务员,公司章程规定了业务员一定的权限和职责,王三进入了业务员的角色,王三也就有了这些权限和职责,但这些权限职责不是和王三本人有直接联系的,而是通过王三的角色才会发生在王三身上;如果王三升迁做经理,表示其进入经理这样的角色,由此经理角色拥有的权限和职责王三又会拥有。

由于有了这样两个分离的系统,因此在具体应用上可以分别实现,在J2EE中,与角色相关的访问权限是通过配置文件(Web.xml和ejb-jar.xml)由容器自动实现的,而且这种访问权限的配置也是非常方便灵活的。

而与角色相关的用户系统则由具体应用系统的开发者来实现,可以采取基于数据库或LDAP等技术的数据系统来实现,例如用户注册资料的新增和修改等。

本项目的设计思路就是完全按照这两种分离的思路实现的,将与角色相关的访问权限系统交由J2EE容器实现。因此,如何配置J2EE将是本项目实现中的一个主要部分;代码设计编程则主要集中在基于数据库的用户管理系统上。

2.2  J2EE的JAAS

J2EE容器实现了与角色相关的访问权限功能,如何在自己的具体应用系统开发中使用J2EE的安全系统?

通过使用J2EE容器提供的JAAS (Java Authentication Authorization Service,Java验证和授权API),JAAS是J2EE服务器用来帮助应用系统实现安全功能的。当应用系统的开发者具体实现了LoginModule API,那么J2EE容器就执行LoginModule接口,通过接口和具体实现之间的关系,J2EE容器将结合具体应用系统实现特定的JAAS功能。

标准的LoginModule接口可以让应用系统开发者自由地选择数据系统,如数据库、LDAP(lightweight directory access protocol)或者共享文件系统。这些变化却无需修改程序。

这就形成了基于J2EE安全系统开发模式:编写一个Login Module,然后打包到自己的具体应用中,然后以一种约定的方式发布到J2EE平台上。

图6-1显示在用户登录的情况下,应用系统实现的LoginModule与J2EE容器如何交互实现JAAS。

图6-1  J2EE的Login交互图

当用户实现LoginContext.login()时,Web容器将调用JAAS。JAAS确认应用系统自己实现的LoginModule正确配置后,用户和密码验证等验证工作交由应用系统自己的LoginModule实现。

正常登录后,用户将通过Web容器访问业务逻辑核心的EJB容器,J2EE中可以对每个EJB的方法实现访问权限控制,这些都可以在ejb-jar.xml中配置。当Web容器发出一个EJB的lookup命令时,容器将从subject中获得角色。然后和ejb-jar.xml配置中相应EJB允许操作的角色相比较,如果两者一致,则允许lookup命令执行,EJB层将返回lookup后的bean。

在本项目中,由于采取JBoss作为J2EE容器,JBoss提供了绑定的LoginModule,如LdapLoginModule 和DatabaseServerLoginModul,因此本项目就不必再编写LoginModule,只要在Jboss的配置文件中配置相应的LoginModule就可以。

关于LoginContext的实现有多种途径。

第一种是应用系统自己通过调用LoginContext.login()实现拦截验证功能,在拦截功能的模块中包含如下语句:

AppCallbackHandler handler = new AppCallbackHandler(name, passwordChar);

LoginContext lc = new LoginContext(Constants.LOGIN_MODULE, handler);

lc.login();

Subject subject = lc.getSubject();

PrivilegedAction action = new PrivilegedAction () {

public Object run() {

     // do something

     }

};

// perform action as Subject t

Subject.doAs(subject, action);

拦截功能的实现有几种,使用Servlet 2.3以上版本支持的Filter是比较好的办法,还需要重载HTTPServletRequestWrapper的某些方法。

这样做的好处是可以在用户登录后加入一些应用系统需要实现的预先工作,定制性很强,甚至直接在应用系统中使用自己的LoginModule,但是这些工作实现起来比较麻烦。

另外一种则比较简单,只要在Web按照Servlet 2.3的安全章节所规定的,在Web.xml配置login-config,由容器自动生成LoginContext并保存返回的Subject。本项目将采取该方案,关于login-config的配置和J2EE登录配置见2.3节。

2.3  单点登录

单点登录(SSO,single sign on)是指在分布式环境下,整个系统只有一个可以登录进入的点,它对所有的请求(Request)都是通用的。单点登录可以保证用户能够访问到可以访问的资源,如果有一个未被授权的请求要求访问被保护的资源,这个请求将自动被导向到相应的验证点进行登录验证。

J2EE容器支持单点登录模式的实现。通过使用J2EE Web层安全机制,可以保护Web层的一些资源如URL、URL模型以及HTTP的提交方式(POST或GET等)。当未被授权的用户访问这些受保护的资源时,J2EE容器会自动将用户导向到规定的登录界面,要求用户输入用户名和密码。

J2EE容器支持常见的下列几种登录验证机制。

·          基于HTTP的基本验证(HTTP Based Authentication):这是基于HTTP/1.0规定中的用户名和密码验证机制。类似以前在Apache中的设置,该方式的缺点是不够安全,密码只是简单地使用base64编码。当用户访问被保护资源时,浏览器跳出一个提示框,要求输入用户名和密码,如图6-2所示。

·          基于HTTPS的客户端验证(HTTPS Client Authentication0):客户端通过HTTPS (HTTP over SSL)和服务器端发生互动,这个机制需要公用密钥证书,因此要安全得多。

·          基于表单验证(form-based authentication):登录界面可以使用JSP/HTML特定定制,因此在界面上可以更加美观,如图6-3所示。

      

图6-2  基于HTTP/1.0的登录界面                     图6-3  登录表单

但是对登录界面表单有限制。例如login.jsp是用户登录的界面,那么在login.jsp中需要如下语句:

其中,Action的值、用户名以及密码名都必须分别严格采用j_security_check、j_username和j_password英文写法。除此之外可以任由开发人员发挥了。

Login.jsp提交到j_security_check后,Web容器将执行,如图6-1所示的流程,如果验证成功,返回Web层的一个subject。

登录类型的设置比较简单,在web.xml中加入如下语句:

 

    BASIC

    Register User

 

这表示是基于HTTP的基本验证,而使用下列语句表示基于表单的验证:

    FORM

    SecurityRealm

   

      /account/login.jsp

      /account/login_error.jsp

   

 

需要应用系统开发人员自己设置login的页面和出错页面,/account/login.jsp表示在路径account的login.jsp页面。

2.4  邮件发送组件

在本项目用户管理的需求中,需要对丢失的密码进行查询。输入用户的信箱,系统将查询后的密码发往用户的信箱。这将使用JavaMail连接专门的SMTP服务器进行信件发送,这个信件的发送过程有可能因为网络原因或其他未知原因导致处理时间延长,如果系统中的其他处理事务都要等待这个过程的完成,显然是没有效率而且问题会很多。

采用JMS的邮件发送组件是基于一个异步消息机制的可重用系统,该组件系统采取session bean作为Queue的消息生产者,而MDB作为Queue的消息使用者,作为EJB的一个实现,可以重复使用在需要邮件发送功能的应用系统中,如图6-4所示。

图6-4  邮件发送组件图

客户端只要直接调用EJB AsyncSender就可以实现邮件发送功能。 

3  详细设计和实现

图6-5  业务模型图

本项目功能需求中用户资料管理功能将需要详细的类设计和实现,而权限的验证和授权以及可控制的资源限制访问两个功能是由J2EE容器实现的,需要进行详细的配置和设置。下面就分这两个方向将本项目逐步具体实现。

3.1  业务对象建模

首先确定本项目的Domain Model,或者可以称为基本业务对象,如图6-5所示。

Role代表角色,User代表用户,group代表用户组,用户组是用户的集合,多个用户可以对应同一个角色。角色定义需要是灵活的,可以增减修改的。角色Role接口如下:

public interface Role extends java.io.Serializable{

  public String getRoleId();

  public void setRoleId(String roleId);

  //获得角色名称

  public String getName();

  public void setName(String name);

}

用户User的接口定义:

public interface User extends java.io.Serializable{

    //用户的名称

    public String getName();

    public void setName(String name) ;

    //用户Id

    public String getUserId() ;

    public void setUserId(String userId) ;

    //用户密码

    public String getPassword();

    public void setPassword(String password);

    //用户E-mail

    public String getEmail();

    public void setEmail(String email);

}

在用户接口定义中,只定义关键常用字段的操作行为,有关用户的地址、电话等其他信息使用其他对象表示,可以根据具体应用系统不同的要求再设计。

用户组group的接口定义如下:

public interface Group extends java.io.Serializable{

    public String getGroupId();

    public void setGroupId(String groupId);

    //用户组名称

    public String getName();

    public void setName(String name);

}

用户组是用来代表一组用户,可以指定一个用户组为一个特定角色,那么该用户组下的所有用户也将拥有该角色的访问权限能力。

用户和角色的直接关系设定为多对一,每个用户只能为一个角色,这样可以使问题变得简单些。


3.2  数据库设计

依据业务对象模型建立相应的数据模型,同时使用专门关联表来实现业务对象之间的关系。在EJB 2.0以上容器中,可以使用实体Bean的CMR来表示一对多、多对一或多对多的关系。这样就无需写很多代码,但是使用数据表来表示关系更容易把握,更方便理解和使用。

以MySQL数据库为例,下面是用户User的数据表结构:

CREATE TABLE user (

  userId varchar(50) binary NOT NULL default ',           #用户Id

  password varchar(50) binary default NULL,                 #密码

  name varchar(50) default NULL,                           #用户名

  email varchar(50) default NULL,                           #E-mail邮件地址

  PRIMARY KEY  (userId),

  UNIQUE KEY email (email),

  UNIQUE KEY name (name)

) TYPE=InnoDB;                                             #使用MySQL的InnoDB

以下是角色Role的数据表结构:

CREATE TABLE role (

  roleId varchar(50) binary NOT NULL default ',             #角色Id

  name varchar(100) default NULL,                         #角色名称

  PRIMARY KEY  (roleId)

) TYPE=InnoDB;

使用下表保存用户和角色之间关系:

CREATE TABLE users_roles (

  userId varchar(50) NOT NULL default ',                    #用户Id

  roleId varchar(50) NOT NULL default '                     #角色Id

) TYPE=InnoDB;

用户组group、用户组与用户关系以及用户组与角色关系与此类似。

3.3  实体bean实现

在EJB层将实现用户资料管理的主要功能,可以使用EJB CMP实现各个数据模型,其中User的实体bean如下。

Bean实现:

import javax.ejb.*;

abstract public class UserBean implements EntityBean {

  EntityContext entityContext;

  public java.lang.String ejbCreate(java.lang.String userId) throws CreateException {

    setUserId(userId);

    return null;

  }

  public void ejbPostCreate(java.lang.String userId) throws CreateException {

  }

  public void ejbRemove() throws RemoveException {

  }

  public abstract void setUserId(java.lang.String userId);

  public abstract void setPassword(java.lang.String password);

  public abstract void setName(java.lang.String name);

  public abstract void setEmail(java.lang.String email);

  public abstract java.lang.String getUserId();

  public abstract java.lang.String getPassword();

  public abstract java.lang.String getName();

  public abstract java.lang.String getEmail();

  public void ejbLoad() {

  }

  public void ejbStore() {

  }

  public void ejbActivate() {

  }

  public void ejbPassivate() {

  }

  public void unsetEntityContext() {

    this.entityContext = null;

  }

  public void setEntityContext(EntityContext entityContext) {

    this.entityContext = entityContext;

  }

}

Local Home接口为:

 

import javax.ejb.*;

import java.util.*;

 

public interface UserHome extends javax.ejb.EJBLocalHome {

  public UserLocal create(String userId) throws CreateException;

   public UserLocal findByEmail(String email) throws FinderException;

   public UserLocal findByName(String name) throws FinderException;

  public UserLocal findByPrimaryKey(String userId) throws FinderException;

}

Local接口为:

import javax.ejb.*;

import java.util.*;

 

public interface UserLocal extends javax.ejb.EJBLocalObject {

  public String getUserId();

  public void setPassword(String password);

  public String getPassword();

  public void setName(String name);

  public String getName();

  public void setEmail(String email);

  public String getEmail();

}

相应的ejb-jar.xml为:

  User

  User

  com.jdon.security.auth.ejb.UserHome

  com.jdon.security.auth.ejb.UserLocal

  com.jdon.security.auth.ejb.UserBean

  Container

  java.lang.String

  False

  2.x

  User

 

     userId

 

 

     password

 

 

     name

 

 

     email

 

     userId

 

     

     findByEmail

    

        java.lang.String

     

     

      SELECT OBJECT(s) FROM User AS s WHERE s.email=?1

 

 

     

          findByName

         

          java.lang.String

         

       

        SELECT OBJECT(s) FROM User AS s WHERE s.name=?1

 

在该实体bean中,使用EJB-QL实现了以E-mail或用户名为关键字的查询语句。

其他数据表都可以采取类似上述方法建立,当然使用Jbuilder专门的EJB可视化开发工具,可以直接从数据库中将这些表自动导入成相应的实体bean,降低开发量。

3.4  Session Bean实现

本项目需要一个Facade类统一实现用户资料的操作,建立无状态Session Bean 为UserManager的EJB。在这个类中,主要实现有关用户的新增、删除或修改。代码如下:

public class UserManagerBean implements SessionBean {

  private final static Logger logger = Logger.getLogger(UserManagerBean.class);

 

  SessionContext sessionContext;

  UserHome userHome;

  UsersRolesHome usersRolesHome;

  UsersGroupsHome usersGroupsHome;

  RoleManagerLocalHome roleManagerLocalHome;

  SequenceGeneratorLocalHome sequenceHome;

  // ejbCreate()一般只执行一次,以后再调用时不再执行,通过ejbCreate()可以

   //实现一些类属性的缓冲

  public void ejbCreate() throws CreateException {

    try {

      ServiceLocator serviceLocator = new ServiceLocator();

      userHome = (UserHome) serviceLocator.getLocalHome(

          JNDINames.USER_HOME);

      sequenceHome = (SequenceGeneratorLocalHome)

    serviceLocator.getLocalHome(JNDINames.SEQUENCEGENERATOR_HOME);

      usersRolesHome = (UsersRolesHome) serviceLocator.getLocalHome(

            JNDINames.USERSROLES_HOME);

      usersGroupsHome = (UsersGroupsHome) serviceLocator.getLocalHome(

          JNDINames.USERSGROUPS_HOME);

      roleManagerLocalHome = (RoleManagerLocalHome)

      serviceLocator.getLocalHome( JNDINames.ROLEMANAGER_HOME);

    } catch (Exception ex) {

      logger.error("Service Locate error:" + ex);

      throw new CreateException();

    }

  }

  //从Sequence EJB组件获得自增的序列号

  public int getNewId(String name) {

    try {

      SequenceGeneratorLocal seq = sequenceHome.create();

      return seq.nextSequenceNumber(name);

    } catch (Exception ex) {

      throw new EJBException("Error generating id for : " + name + ex);

    }

  }

  //创建一个用户

  public void createUser(UserEvent userEvent) {

    User userDTO = userEvent.getUser();

    if (nameIsExisted(userDTO)){

      logger.debug("name :" + userDTO.getName() + " has been exsited");

      userEvent.setErrors(Constants.NAME_EXISTED);

      return;

    }

    if (emailIsExisted(userDTO)){

      logger.debug("eamil :" + userDTO.getEmail() + " has been exsited");

      userEvent.setErrors(Constants.EMAIL_EXISTED);

      return;

    }

    UserLocal userLocal = null;

    try {

      String id = Integer.toString(getNewId(Constants.SEQUENCE_USER_NAME));

      userLocal = userHome.create(id);

      userDTO.setUserId(id);

      updateUser(userEvent);

      //创建该用户的默认角色

      createUsersRoles(userDTO);

    } catch (Exception ex) {

      logger.error(ex);

      throw new EJBException("create user error : " + ex);

    }

  }

  //检查是否有重复用户名

  private boolean nameIsExisted(User userDTO) {

    boolean found = false;

    User user = getUserByName(userDTO.getName());

    if (user != null)

      found = true;

    return found;

  }

  //检查是否有重复E-mail

  private boolean emailIsExisted(User userDTO) {

    boolean found = false;

    User user = getUserByEmail(userDTO.getEmail());

    if (user != null)

      found = true;

    return found;

  }

  //创建用户默认角色,也就是建立用户和角色的对应关系

  public void createUsersRoles(User userDTO) throws Exception {

    UsersRoles usersRoles = null;

    try {

      RoleManagerLocal roleManagerLocal = roleManagerLocalHome.create();

      String roleId = roleManagerLocal.getRoleId(Role.USER);

      usersRoles = usersRolesHome.create(userDTO.getUserId(), roleId);

    } catch (Exception ex) {

      logger.error(ex);

      throw new EJBException("createUsersRoles error : " + ex);

    }

  }

  //修改用户资料

  public void updateUser(UserEvent userEvent) {

    User userDTO = userEvent.getUser();

    UserLocal userLocal = null;

    try {

      userLocal = userHome.findByPrimaryKey(userDTO.getUserId());

      userLocal.setName(userDTO.getName());

      userLocal.setEmail(userDTO.getEmail());

      userLocal.setPassword(userDTO.getPassword());

 

    } catch (Exception ex) {

      logger.error(ex);

    }

  }

  //以E-mail获得用户

  public User getUserByEmail(String emailAddress) {

    emailAddress = emailAddress.trim().toLowerCase();

    UserLocal userLocal = null;

    try {

      userLocal = userHome.findByEmail(emailAddress);

    } catch (Exception ex) {

      logger.warn(ex);

    }

    return getUser(userLocal);

  }

  //以Id查询用户

  public User getUserById(String userId) {

    userId = userId.trim().toLowerCase();

    logger.debug(" userId =" + userId);

    UserLocal userLocal = null;

    try {

      userLocal = userHome.findByPrimaryKey(userId);

    } catch (Exception ex) {

      logger.warn(ex);

    }

    return getUser(userLocal);

  }

  //获得用户User实例 DTO模式

  private User getUser(UserLocal userLocal) {

    if (userLocal == null)

      return null;

    logger.debug(" userId =" + userLocal.getUserId());

    User user = new UserModel(userLocal.getUserId());

    user.setEmail(userLocal.getEmail());

    user.setName(userLocal.getName());

    user.setPassword(userLocal.getPassword());

    user.setUserId(userLocal.getUserId());

    return user;

  }

  //获得用户的Principal

  public User getUserByPrincipal() throws Exception, PrincipalException {

    Principal principal = null;

    try {

      principal = sessionContext.getCallerPrincipal();

    } catch (Exception e) {

      logger.error(e);

      throw new PrincipalException();

    }

    if (principal == null) {

      throw new PrincipalException();

    }

    String name = principal.getName();

    return getUserByName(name);

   }

   …

}

在UserManager中基本实现了用户资料的相关操作。在本项目中,还有用户组以及角色的相关操作。为避免UserManager中包含过多逻辑,需要再建立一个无状态Session Bean,如RoleManager。

用户注册登录后,其Session生存周期的活动将一直维持到其离开系统。因此可以建立一个有状态Session Bean保存用户的资料如User实例,这样不必经常到数据库读取。有状态Session Bean还可以作为一个总的Facade类,分别包含其他Facade群,这样,系统会显得条理分明。创建有状态Session Bean代码如下:

public class SecurityFacadeBean extends EJBControllerBean {

  private final static Logger logger = Logger.getLogger(SecurityFacadeBean.class);

 

  SessionContext sessionContext;

  RoleManagerLocalHome roleManagerLocalHome;

  UserManagerLocalHome userManagerLocalHome;

  AsyncSenderLocalHome asyncSenderLocalHome;

  private User user = null;

  …

  //获得Facade类 UsermanagerLocal

  public UserManagerLocal getUserManager() {

    UserManagerLocal userManagerLocal = null;

    try {

      userManagerLocal = userManagerLocalHome.create();

    } catch (Exception ex) {

      logger.error("getUserManager() error:" + ex);

    }

    return userManagerLocal;

  }

  //获得Facade类RoleManagerLocal

  public RoleManagerLocal getRoleManager() {

    RoleManagerLocal roleManagerLocal = null;

    try {

      roleManagerLocal = roleManagerLocalHome.create();

    } catch (Exception ex) {

      logger.error("getRoleManager() error:" + ex);

    }

    return roleManagerLocal;

  }

  //获得当前session的User实例

  public User getUser() {

    if (this.user != null)

      return this.user;

    logger.debug("user is null, get it from principal ...");

    setUser();

    return this.user;

  }

 //密码丢失查询

 public boolean getPassword(String email) {

    logger.debug("--> enter getpassword");

    boolean success = false;

    try {

      User user = getUserManager().getUserByEmail(email);

      if (user == null)

        return success;

      String subject = " 用户名和密码";

      StringBuffer buffer = new StringBuffer();

      buffer.append(" 用户:").append(user.getName());

      buffer.append(" 密码:").append(user.getPassword());

      if (sendMail(user.getEmail(), subject, buffer.toString()))

        success = true;

    } catch (Exception ex) {

      logger.error(" getPassword: " + ex);

    }

    return success;

  }

  //调用E-mail发送组件,通过JMS发出E-mail

  public boolean sendMail(String toAddress, String subject, String content) {

    try {

      logger.debug(" -->enter send mail");

      Mail mail = new Mail();

      mail.setToAddress(toAddress);

      mail.setFromAddress("banq@jdon.com");

      mail.setSubject(subject);

      mail.setContent(content);

      String msg = MailUtil.getMsgFromMail(mail);

      AsyncSenderLocal asyncSenderLocal = asyncSenderLocalHome.create();

      asyncSenderLocal.sendAMessage(msg);

      logger.debug(" -->send mail to: " + toAddress + " successfully!");

      return true;

    } catch (Exception ex) {

      logger.error(" sendMail() error : " + ex);

      return false;

    }

  }

  //判断当前用户是否是管理员

  public boolean isAdministrator() {

    return sessionContext.isCallerInRole(Role.ADMINISTRATOR);

  }

  //判断当前用户是否是普通用户

  public boolean isUser() {

    return sessionContext.isCallerInRole(Role.USER);

  }

  …

}

可以看到,SecurityFacadeBean是继承接口框架系统中的EJBController,SecurityFacadeBean作为一个总的Facade类,通过接口框架负责和Web实现联系,如图6-6所示。

图6-6  EJB Facade群

SecurityFacadeBean中isCallerInRole是用来判断当前用户是否属于可以访问该资源的角色,访问该资源角色的权限在ejb-jar.xml中定义。


3.5  EJB容器安全配置

J2EE容器的安全管理框架以角色为联系纽带,分两个方向。一个是用户资料系统;另外一个是访问权限系统。后者是通过web.xml或ejb-jar.xml配置实现的。

在本项目中,为了限制角色对某些类或方法的访问权限,可以在ejb-jar.xml中设置。

     the role is super user

      Admin

     register user

     User

     User

    

          RoleManager

           *

     

在本EJB中定义了两个角色:Admin和User(当然可以根据实际情况定义角色)。具体的权限是在method-permission中设置,可以指定某个类的具体方法,“*”表示所有的方法。

既然定义了角色,那么在具体类的配置中也需要声明具体角色的指向,如SecurityFacadeBean的ejb-jar配置如下:

            SecurityFacade

            SecurityFacade

           

com.jdon.security.auth.ejb.SecurityFacadeLocalHome

            com.jdon.security.auth.ejb.SecurityFacadeLocal

            com.jdon.security.auth.ejb.SecurityFacadeBean

            Stateful

            Container

           

               

                ejb/UserManager

                Session

               

                   com.jdon.security.auth.ejb.UserManagerLocalHome

                

                com.jdon.security.auth.ejb.UserManagerLocal

                UserManager

           

           

               

                ejb/RoleManager

                Session

               

                    com.jdon.security.auth.ejb.RoleManagerLocalHome

                

                com.jdon.security.auth.ejb.RoleManagerLocal

                RoleManager

           

            

               

                ejb/AsyncSender

                Session

               

                   com.jdon.asyncsender.ejb.AsyncSenderLocalHome

                 

                com.jdon.asyncsender.ejb.AsyncSenderLocal

                AsyncSender

           

           

               

                User

                User

           

在最后几行,指定角色名User指向了security-role中的User,两者名称可以不一样,但role-link必须和security-role是一致的。role-name可以是RegisterUser,那么在SecurityFacadeBean调用就是isCallerInRole("RegisterUse"),检查当前访问SecurityFacadeBean的角色是否是容器中已经定义的、允许访问的角色RegisterUse。

通过ejb-jar.xml的配置,使得角色的访问权限的设置和管理变得非常方便。在系统运行时,有可能有新的角色加入或原有角色权限的修改。这些都可以通过ejb-jar.xml进行修改,修改完毕,运行中的J2EE系统立即生效。

具体ejb-jar.xml的权限配置可参考Sur公司EJB 标准手册。

为了使该EJB的权限机制激活,还需要在相应的容器配置文件中进行适当配置。如在JBoss中部署该EJB时,需要在jboss.xml加入:

java:/jaas/SecurityRealm

这表示该EJB的安全域将使用JAAS中的SecurityRealm配置。关于如何设置JBoss的SecurityRealm将在部署配置相关章节中讨论。 

4  JMS 邮件发送组件

在J2EE中,JMS(Java Message System)提供了一种异步处理机制的实现。JMS通过异步的、非阻塞的消息传递,将消息的生产者和使用者松散的联系在一起。对于使用者,它无所谓是谁产生了消息或者是在何时产生的。这就能够建立一种动态的、灵活的可靠的系统。所谓可靠,因为JMS将每个消息都保存起来,只有确保本消息处理后才会完全放弃。否则,将会反复提交处理。这种可靠的机制使得JMS能够成功的在证券、银行系统中得到广泛应用。

JMS中的消息类型有两种:Topic和Queue。Topic的操作是使用发布/订阅(publish/subscribe)的方式;Queue的操作是点对点(ponit to point)的方式。

·          publish/subscribe:发布者(publisher)发布消息到Topic,订阅者(subsribe)从Topic订阅消息,订阅者的数量是不受限制的。

·          ponit to point:点对点传输消息,建立在消息队列Queue的基础上,每个消息使用者只对应一个消息队列,不像publish/subscribe那样可以有很多消息使用者。

本项目中,可以由单个独立的程序来实现邮件发送,这个程序作为一个消息使用者,只需一个就可以了,因此使用Queue来实现。

JMS在消息到达消息使用者,有两种——同步和异步。

·          同步是指消息使用者明确地主动地调用从Queue或Topic中得到消息,一直进行循环直至一个消息到达,或者也可以设定超时时间。很明显这个方法是比较耗费CPU资源的。

·          异步接受是指消息到达时,主动通知消息使用者,消息使用者可以实现message listener接口。这样每当消息到达时,JMS provider 会通过调用这个listener的onMessage方法来传输这个消息。该方法相对要有效率,邮件发送组件将采取这种方式。

下面描述这个邮件发送组件是如何具体实现的。

在这个组件中,消息生产者应该是具体应用程序。但是组件为了达到通用目的,需要建立一个专门的消息生产者,可以称之为AsyncSender,(异步发送器)。具体应用程序通过调用这个异步发送器,将邮件内容发给JMS的Queue,负责邮件发送的类作为这个Queue另外一端的消息使用者,在Queue中有需要发送的邮件出现时就立即将它从Queue中取出,实现邮件发送。


4.1  消息发送器

AsyncSender属于消息生产者,它是具体应用系统直接调用的,可以使用无状态Session Bean来实现。

在AsyncSender中,首先要通过JNDI获得一个ConnectionFactory,创建一个QueueConnection实例,这样就获得与JMS Provider的一个连接。再由这个QueueConnection创建一个QueueSession,这样就启动了一个和JMS Provider连接相关的线程,这个线程可以是发送或接收消息。

AsyncSender还需要通过JNDI获得一个已经存在的Queue。这样,通过将发送消息的线程和这个Queue联系起来,表示向这个Queue中发送消息。

AsyncSender的bean代码如下:

/**

 *  JMS客户端 消息生产者

 *

Copyright: Jdon.com Copyright (c) 2003

 *

Company: 上海解道计算机技术有限公司

 * @author banq

 * @version 1.0

 */

public class AsyncSenderBean implements SessionBean {

 

  private final static Logger logger = Logger.getLogger(AsyncSenderBean.class);

 

  SessionContext sessionContext;

  private SessionContext sc;

  private Queue queue;

  private QueueConnectionFactory qFactory;

 

  public void ejbCreate() throws CreateException {

    try {

      ServiceLocator serviceLocator = new ServiceLocator();

        //查询JNDI获得QueueConnectionFactory

      qFactory =

          serviceLocator.getQueueConnectionFactory(

          JNDINames.QUEUE_CONNECTION_FACTORY);

       //查询JNDI 获得已经存在的Queue

      queue = serviceLocator.getQueue(JNDINames.ASYNC_SENDER_QUEUE);

    } catch (ServiceLocatorException sle) {

      throw new EJBException("AsyncSenderEJB.ejbCreate failed", sle);

    }

  }

  //发送邮件的方法

  public void sendAMessage(String msg) {

    QueueSession session = null;

    QueueConnection qConnect = null;

    QueueSender qSender = null;

    try {

      //创建一个QueueConnection

      qConnect = qFactory.createQueueConnection();

      //创建一个QueueSession

      session = qConnect.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);

      logger.debug("-->>create sender");

      //创建一个发送者

      qSender = session.createSender(queue);

      TextMessage jmsMsg = session.createTextMessage();

      //设置发送内容

      jmsMsg.setText(msg);

      //向目标queue发送

      qSender.send(jmsMsg);

      logger.debug("-->>send ok msg:"+msg);

    } catch (Exception e) {

      logger.error("sendAMessage error " + e);

      throw new EJBException("askMDBToSendAMessage: Error!", e);

    } finally {

      try {

        if (qConnect != null) {          qConnect.close();        }

      } catch (Exception e) {}

    }

  }

  …

}

其中,QueueConnectionFactory的JNDI名称是java:comp/env/jms/QueueConnection Factory,而Queue的JNDI名称是java:comp/env/jms/MailQueue,分别需要在ejb-jar.xml中落实这两个环境变量。

在ejb-jar.xml中,有:

     AsyncSender

     AsyncSender

    

        com.jdon.asyncsender.ejb.AsyncSenderLocalHome

     

     com.jdon.asyncsender.ejb.AsyncSenderLocal

    com.jdon.asyncsender.ejb.AsyncSenderBean

     Stateless

     Container

     

    

         

                jms/QueueConnectionFactory

                javax.jms.QueueConnectionFactory

                Container

  

  

    

         

          jms/MailQueue

          javax.jms.Queue

   

QueueConnectionFactory和Queue与具体JMS容器有关。在JBoss 3.0中,提供了现成的QueueConnectionFactory和Queue,当然也可以参考JBoss手册自己设置建立这两个配置。

这里使用JBoss的ConnectionFactory和已经存在的/testQueue,其JNDI名称写法分别是java:/ConnectionFactory和queue/testQueue,这些都是JBoss的规定写法。如果是Topic,那么就需要写成topic/testTopic,testTopic/testQueue是JBoss容器在启动时建立好的一个Topic/Queue。在jboss.xml中,写入下列代码:

 

        AsyncSender

        AsyncSenderLocal

       

             jms/QueueConnectionFactory

             java:/ConnectionFactory

       

       

             jms/MailQueue

             queue/testQueue

       

  

通过上面开发,消息的生产者功能基本完成,通过调用EJB AsyncSender的sendAMessage(String msg)方法,可以将msg发送到JMS的Queue中。


4.2  MDB

在Queue的另外一端是消息的使用者。MDB(Message-Driven Beans)专门处理JMS异步消息,Session Bean和Entity Bean只允许同步地去发送消息和接收消息,不支持异步。MDB是一个message listener,它能够从一个Queue或一个durable subscription中可靠地接收消息。

MDB与一个普通的消息使用者客户端的区别是,EJB容器将自动做下面的事情,无需应用者再自己编程实现:

·          创建一个消息接受者(QueueReceiver/TopicSubscriber)接收消息。在部署时,将destination和ConnectionFactory与MDB联合起来。在JBoss中通过指定destination-jndi-name来实现。

·          自动实现message listener接口(无需调用setMessageListener方法)。

·          容器自动指定了消息签收模式。

因此,使用MDB作为消息的使用者就非常简单,而且没有home和remote接口,只有一个bean类。建立MDB MailerBean代码如下:

/**

 * JMS消息使用者 EJB消息Bean

 *

Copyright: Jdon.com Copyright (c) 2003

 *

Company: 上海解道计算机技术有限公司

 * @author banq

 * @version 1.0

 */

public class MailerBean implements MessageDrivenBean, MessageListener {

  private final static Logger logger = Logger.getLogger(MailerBean.class);

  MessageDrivenContext messageDrivenContext;

  public void ejbCreate(){

  }

  public void ejbRemove() {}

  //当消息来时,将自动激活这个方法

  public void onMessage(Message msg){

    logger.debug(" --> enter onMessage ..");

    TextMessage textMessage = null;

    String xmlMailMessage = null;

    Mail recMail = null;

    try {

      textMessage = (TextMessage) msg;

      xmlMailMessage = textMessage.getText();

      //将XML文本转换成Mail对象实例

      recMail = MailUtil.getMailFromMsg(xmlMailMessage);

      logger.debug(" --> begin connect the server ....");

      sendMail(recMail);

      logger.debug(" --> send mail ok ");

    } catch (JMSException je) {

      logger.error("MailerMDB.onMessag error" + je);

      throw new EJBException("MailerMDB.onMessage" + je);

    } catch (Exception me) {

      logger.error("MailerMDB.onMessag error" + me);

    }

  }

  //发送邮件

  private void sendMail(Mail mail) throws Exception{

    getMailHelper().createAndSendMail(mail);

  }

  private MailHelper getMailHelper(){

    return (new MailHelper());

  }

  public void setMessageDrivenContext(MessageDrivenContext messageDrivenContext) {

    this.messageDrivenContext = messageDrivenContext;

  }

}

MailHelper是一个专门使用J2EE容器的mail service发送邮件的类,代码如下:

public class MailHelper {

   private final static Logger logger = Logger.getLogger(MailHelper.class);

   //创建一个email message 并且使用J2EE mail services发送它

  public void createAndSendMail(Mail mail) throws MailerAppException {

    try {

      logger.debug(" --> lookup mail session");

      InitialContext ic = new InitialContext();

      Session session = (Session) ic.lookup(JNDINames.MAIL_SESSION);

      Message msg = new MimeMessage(session);

 

      logger.debug(" --> beigin to set mail ");

      msg.setFrom(new InternetAddress(mail.getFromAddress()));

      msg.setRecipients(Message.RecipientType.TO,

                        InternetAddress.parse(mail.getToAddress(), false));

      msg.setSubject(mail.getSubject());

      msg.setText(mail.getContent());

      msg.setSentDate(new Date());

      logger.debug(" --> beigin to send now ....");

      Transport.send(msg);

    } catch (Exception e) {

      logger.error("createAndSendMail exception : " + e);

      throw new MailerAppException("Failure while sending mail");

    }

  }

}

由上可见,MDB MailerBean是在Queue中有消息到达时,取出后委托MailHelper使用J2EE容器的Mail Service发送邮件。

这两个类都比较依赖容器,所以要进行容器的配置。

在ejb-jar.xml中配置MailerBean如下:

  Mailer

    Mailer

    com.jdon.mailer.ejb.MailerBean

    Container

   

        javax.jms.Queue

   

    

   

       

          mail/DefaultMail

          javax.mail.Session

          Container

   

由于使用了容器的邮件发送服务,那么需要指定这个服务的JNDI,在jboss.xml当中加入:

   Mailer

    queue/testQueue

   

         mail/DefaultMail

         java:/Mail

   

这表示使用JNDI为java:/Mail的邮件服务资源,那么再部署发布本组件时,就需要在JBoss服务器中配置这个邮件服务资源,具体如何配置参见部署发布的章节。  

5  Web层的实现

J2EE的登录功能主要是在Web层实现。在前面架构设计中分析到,通过定制LoginModule登录进入Web层,同时获得了登录用户的角色,而且也将以相应的角色访问EJB层。Web层的实现也分两个步骤:一个是用户资料的管理;另外一个是访问权限的配置。

5.1  用户资料管理

用户资料管理主要是实现新用户注册;用户资料查询修改;丢失密码查询等功能。这些功能都是通过上面章节介绍的接口框架对后台EJB进行操作,建立UserService作为ServiceProxy的实现,如下:

/**

 *  接口框架的ServiceProxy的具体应用实现者

 *

Copyright: Jdon.com Copyright (c) 2003

 *

Company: 上海解道计算机技术有限公司

 * @author banq

 * @version 1.0

 */

public class UserService extends ServiceSupport {

  private final static String module = UserService.class.getName();

  public void perform(Event e) {

    Debug.logVerbose("-->enter UserService ", module);

    UserManagerLocal userManagerLocal = getUserManagerLocal();

    UserEvent userEvent = (UserEvent) e;

    switch (userEvent.getActionType()) {

      case UserEvent.QUERY:

        performQuery(userEvent);

        break;

      case UserEvent.CREATE:

        Debug.logVerbose("-->enter create userId: ", module);

        userManagerLocal.createUser(userEvent);

        break;

      case UserEvent.EDIT:

        Debug.logVerbose("-->enter edit  ", module);

        if (validateUser(userEvent))

          userManagerLocal.updateUser(userEvent);

        else {

          Debug.logVerbose(

              " the user in UserForm is not the user in session. ", module);

          userEvent.setErrors(Constants.SYSTEM_ERROR);

        }

        break;

      default:

        Debug.logVerbose("no this actionType " + userEvent.getActionType(),

                         module);

        break;

    }

  }

  //使用内存状态中的User与用户输入的User资料进行比较,确认合法性

  private boolean validateUser(UserEvent userEvent) {

    boolean success = false;

    User user = userEvent.getUser();

    SecurityFacadeLocal sf = (SecurityFacadeLocal) eJBControllerLocal;

    if (sf.getUser() != null) {

      String userId = sf.getUser().getUserId();

      user.setUserId(userId);

      success = true;

    }

    return success;

  }

 

  private UserManagerLocal getUserManagerLocal() {

    UserManagerLocal userManagerLocal = null;

    try {

      SecurityFacadeLocal securityFacadeLocal = (SecurityFacadeLocal)

          eJBControllerLocal;

      Debug.logVerbose("-->get SecurityFacadeLocal ", module);

      userManagerLocal = securityFacadeLocal.getUserManager();

    } catch (Exception ex) {

      Debug.logError(" getUserManagerLocal() error: " + ex, module);

    }

    return userManagerLocal;

  }

  //查询用户

  private void performQuery(UserEvent userEvent) {

    User user = userEvent.getUser();

    if (user == null) {

      Debug.logVerbose("-->enter query the user in session ", module);

      SecurityFacadeLocal sf = (SecurityFacadeLocal) eJBControllerLocal;

      user = sf.getUser();

    } else {

      String email = user.getEmail();

      if (email != null) {

        Debug.logVerbose("-->enter query by email " + email, module);

        user = getUserManagerLocal().getUserByEmail(email);

      }

    }

    userEvent.setUser(user);

  }

}

在UserService中将前台界面提交的关于用户资料新增、修改或查询命令传递到后台EJB中处理,其中:

SecurityFacadeLocal sf = (SecurityFacade Local) eJBControllerLocal;

user = sf.getUser();

图6-7  用户注册界面

是从有状态Session Bean SecurityFacade中获得User实例。由于SecurityFacade在第一次被调用后,将一直保存着User实例的数据,因此,只要该用户没有退出系统,其他的数据将一直保存在内存中,供其他程序反复调用。

以用户注册为例,用户注册实际就是用户资料的新增,其signup.jsp页面外观如下:

在signup.jsp中,将图6-7中表单提交到Web层后由Struts的Action来实现,建立SaveSignUpAction如下:

public class SaveSignUpAction extends Action {

  public final static String module = SaveSignUpAction.class.getName();

  public ActionForward execute(ActionMapping actionMapping,

                               ActionForm actionForm,

                               HttpServletRequest request,

                               HttpServletResponse response) throws

      Exception {

    FormBeanUtil.remove(actionMapping, request);

    UserForm userForm = (UserForm) actionForm;

    //检查提交的表单中有无逻辑错误

    if (!checkErrors(userForm, request).isEmpty())

      return (actionMapping.getInputForward());

    //创建一个UserEvent

    UserEvent userEvent = createUserEvent(userForm, request);

    //递交给接口框架处理

    ServiceProxyHandler.perform(userEvent, request);

    //处理完成后,如果没有错误,显示注册成功信息

    if (userEvent.getErrors() == null) {

      if (userEvent.getActionType() == userEvent.CREATE)

         return actionMapping.findForward("createOk");

      else

         return actionMapping.findForward("editOk");

    } else {

      ActionErrors errors = new ActionErrors();

      errors.add(ActionErrors.GLOBAL_ERROR,

                 new ActionError(userEvent.getErrors()));

      saveErrors(request, errors);

      return actionMapping.getInputForward();

    }

  }

//create a UserEvent

  private UserEvent createUserEvent(UserForm userForm, HttpServletRequest request)

    throws      Exception {

    Debug.logVerbose(" --> getUserEvent  ", module);

    String action = userForm.getAction();

    UserEvent userEvent = new UserEvent(module);

    User user = new UserModel();

    try {

      PropertyUtils.copyProperties(user, userForm);

      userEvent.setUser(user);

      userEvent.setActionType(FormBeanUtil.actionTransfer(action));

    } catch (Exception e) {

      Debug.logError("getUserEvent error" + e, module);

      throw new Exception(e);

    }

    return userEvent;

  }

}

在前面章节已经提到,Action的代码已经可以非常模板化,不同的只是基本对象的区别。在eventmappings.xml建立下列对应关系:

   

     com.jdon.security.web.SaveSignUpAction

    

    

      com.jdon.security.service.UserService

    

这样,SaveSignUpAction将UserEvent通过UserService交由EJB处理。

再以用户资料修改为例创建EditSignUpAction.java。用户成功登录后,通过直接调用http://localhost:8080/AuthTest/auth/editSignUpAction.do,将显示如图6-7所示的包含用户注册数据的画面,用户的ID无需从网址URL中输入,因为该用户已经登录,用户资料保留在系统内存中,可以直接调用出来。

EditSignUpAction.java代码如下:

public class EditSignUpAction extends Action {

  public final static String module = EditSignUpAction.class.getName();

  public ActionForward execute(ActionMapping mapping,

                               ActionForm form,

                               HttpServletRequest request,

                               HttpServletResponse httpServletResponse) throws  Exception {

    String action = FormBeanUtil.EDIT_STR;

    int actionInt = FormBeanUtil.actionTransfer(action);

    if (form == null) {

      form = new UserForm();

      FormBeanUtil.save(form, mapping, request);

    }

    UserForm userForm = (UserForm) form;

    //从后台获得已经存在的表单数据

    getSignUpForm(actionInt, userForm, request);

    userForm.setAction(action);

    return (mapping.findForward("success"));

  }

 

  private void getSignUpForm(int actionInt, UserForm userForm,

                             HttpServletRequest request) {

    UserEvent userEvent = new UserEvent(module);

    userEvent.setActionType(userEvent.QUERY);

    try {

       //提交UserService实现Query查询操作

       ServiceProxyHandler.perform(userEvent, request);

       //从userEvent获得结果

     User user = userEvent.getUser();

     Debug.logVerbose(" user name is" + user.getName() + " userId is" +

                      user.getUserId(), module);

      PropertyUtils.copyProperties(userForm, user);

      userForm.setPassword2(user.getPassword());

    } catch (Exception e) {

 

      Debug.logError("getSignUpForm error" + e, module);

    }

  }

}

5.2  Web容器安全配置

假设Web层目录结构如下:

SAMPLE

  |

  |--- WEB-INF

  |----admin  

  |--- account

     |

     |----- auth

account目录是对任何人都开放的,而auth是授权注册用户才能访问。

本例中设定两个角色admin和user,角色admin代表系统的管理员,而user则代表注册用户。这两个角色需要在Web层定义,在web.xml加入如下语句:

    Admin

    User

定义了角色名称,下一步需要进行访问控制权限的分配,希望管理员角色可以访问Web中admin路径下的所有JSP文件或其他任何资源,那么在web.xml中配置:

    admin

   

      Admin Area

     

      /admin/*

     

      /account/auth/*

   

    

      admin

   

   

      NONE

   

 

这样,如果用户以网址http://localhost:8080/admin/访问时,容器将检验通过登录验证用户的角色,如果是admin角色,将会正常访问,否则会被拒绝。

希望一般注册用户user能够访问路径account/auth下任何资源,同时也可以设定角色admin拥有该目录下的资源访问权限。

    User protected

   

      Protected Area

     

      /account/auth/*

     

      /auth/*

   

   

      user

   

   

      NONE

   

 

在Strutss-config.xml中有如下配置:

        type="com.jdon.security.web.SaveSignUpAction"

        validate="true"  input="/account/signup.jsp"  scope="request"

        >

      

     

      type="com.jdon.security.web.EditSignUpAction"

      validate="false" scope="request" >

     

根据web.xml中的配置,路径为/auth/ editSignUpAction的EditSignUpAction只有注册用户才可以访问,因为没有设置根路径“/*”的访问限制,所以SaveSignUpAction则是任何用户都可以访问。

完成了访问控制权限的设置,现在可以设置用户的登录方式了。用户的登录有两种方式,即基于HTTP的登录验证方式和表单登录验证方式。

基本登录方式:

    BASIC

    register user

那么,当用户访问受限制资源时,J2EE容器将自动提示界面如图6-2所示,要求用户输入用户名和密码。

表单登录验证方式:

    FORM

    SecurityRealm

   

      /account/login.jsp

      /account/login_error.jsp

   

当用户访问受限制资源时,J2EE容器自动将/account/login.jsp推向浏览器界面,要求用户输入用户名和密码,如图6-3所示。

最后,为了使Web容器的安全机制激活,需要指定特定的LoginModule,这和具体J2EE服务器相关,在JBoss中,需要配置jboss=web.xml如下:

  java:/jaas/ JdonSecurity

这表示web层的安全域将使用名为JdonSecurity 的JAAS配置。关于JAAS配置JdonSecurity将在6节介绍。  

6  调试配置和运行

采用J2EE容器的安全机制实现本项目的安全框架。虽然代码编程工作减少,但带来的是详细而复杂的J2EE配置,在前面章节已经分别介绍在Web容器和EJB容器下如何实现角色的访问权限限制。在Web容器中,可以指定角色对路径模式(path pattern)的访问权限;在EJB容器中,可以指定角色对具体EJB类及其方法的访问权限。

而基于数据库系统实现的用户资料管理系统则是实现与角色相关用户存储、编辑等功能,这是需要代码编程实现的。

以上容器配置和代码实现这两者之间的联系是以角色为中间纽带。在实际操作中,是通过LoginModule的配置将两者联系在一起的。

J2EE应用系统的配置分两大部分:一个是开发时的配置,如ejb-jar.xml、jboss.xml和web.xml配置等;另外一个是部署时的配置,主要完成开发配置中实现对容器资源的JNDI或其他资源调用,例如CMP对容器的数据库源JNDI调用。在开发配置中,只配置到JNDI名称,那么JNDI名称到底对应哪个具体资源,这些都需要在部署时配置J2EE服务器。

本项目中主要使用了J2EE容器的安全框架,在开发配置中,只配置到security-domain 为java:/jaas/ JdonSecurity,但是java:/jaas/ JdonSecurity具体是怎样实现的,还需要在部署再配置J2EE服务器。

1  JAAS配置

根据前面的架构分析知道,LoginModule是容器JAAS实现的关键,通过LoginModule,容器可以查询用户资料系统中的数据库,从而获得该用户名和密码是否与数据库中是否一致。如果一致,从数据库中获得角色名。当然这也使用于基于LDAP实现的LoginModule。

首先,要配置JBoss能够访问MySQL数据库,配置JBoss下server/default/deploy目录下的mysql-ds.xml文件,加入如下语句:

    SSODS

    jdbc:mysql://localhost:3306/ssodb

    com.mysql.jdbc.Driver

    banq

    1234

   

其中jdbc:mysql://localhost:3306/ssodb表示localhost主机上的数据库名为ssodb,访问用户名和密码分别是banq和1234。这样,JBoss容器就可以访问数据库服务器了。

下面需要定制自己的LoginModule,在JBoss中已经绑定提供两个LoginModule。

·          org.JBoss.security.plugins.samples.LdapLoginModule 是通过JNDI访问LDAP 服务器的LoginModule,详细用法参考JBoss使用手册。

·          org.JBoss.security.plugins.samples.DatabaseServerLoginModule是基于数据库JDBC的LoginModule,基于两个基本逻辑表:Principals 和 Roles,在Principals表中有两个字段principalID和有效的password;而Roles表中有字段principalID和Role以及RoleGroup。

本项目可以直接使用DatabaseServerLoginModule作为LoginModule,配置JBoss/server/default/conf下的login-config.xml文件:

  

  

     code="org.JBoss.security.auth.spi.DatabaseServerLoginModule"

     flag = "required">

       java:/SSODS

      

          select password from user where userId=?

       

      

          select role, 'Roles' from role where userId=?

       

    

  

在dsJndiName配置中,已经连接数据库ssodb。

principalsQuery配置是实现逻辑表Principals,“select password from user where userId=?”是直接操作user数据表的SQL语句。userid和password对应ssodb数据表User中的两个字段,同时userid又对应Principals表的principalID,而password对应Principals表的password。这是逻辑表的一个约定规定,所谓逻辑表就是要提供的SQL查询语句满足逻辑表中的字段要求。这样就将用户资料的数据库系统和容器的JAAS联系了起来,这里类似是一个钩子(HOOK)。

rolesQuery配置是实现逻辑表Roles,在其SQL语句中,也提供了与逻辑表Roles一致的字段。这样,LoginModule就可以通过这条SQL语句直接访问保存在数据库ssodb中的数据了。

配置完成JAAS,就可以在EJB容器和Web容器中使用JdonSecurity这个realm,如在jboss-web.xml或jboss.xml中加入:

java:/jaas/SecurityRealm

这样,整个容器的安全管理框架就可以有机地联系在一起。

2  邮件服务的配置

本项目中使用了邮件发送组件来发送用户丢失的密码,在开发配置中,需要3个配置在部署时配置J2EE服务器。前两个是QueueConnectionFactory和Queue,在JBoss中通过配置文件jbossmq-service.xml和jbossmq-destinations-service.xml(依JBoss具体版本有所区别)设置,这些可以参考JBoss手册配置自己的设置。在本项目中,这两个设置使用了现成的JBoss配置java:/ConnectionFactory和testQueue。

因此,邮件发送组件需要在部署时配置的是Mail Service资源。在开发配置的jboss.xml中,将Mail Service以JNDI名为java:/Mail调用,那么需要在JBoss配置Mail Service,编辑JBoss/server/default/deploy下的mail-service.xml,将其中mail.smtp.host指定为一个SMTP 服务器。如果SMTP服务器需要验证,那么必须加入用户名和密码,如下:

xxxx

xxxx

SMTP 服务器是属于操作系统方面的系统设置,在Linux可以使用SendMail作为SMTP服务器,在Windows平台上可以通过管理工具配置SMTP服务器。

3  部署和发布

建立.ear项目文件,将需要的组件如邮件发送组件打包进来,复制到JBoss/server/default/deploy目录下,如果没有任何错误即表示发布成功。

本项目相当多地使用了容器特性,因此整合测试显得很重要,而且可能比较麻烦,但是只要依据框架标准准确地完成本项目各个部件,运行调试时的问题还是可以基本克服。 

7  小结

本章主要是讨论如何利用J2EE容器的安全框架来实现应用系统的安全管理功能,这些操作实现原理可以应用于任何需要安全机制的系统。

相比前面章节讨论的“简单的用户注册系统”,本章部分操作显得过于复杂,但是这些技术的选用都是依据不同应用要求作出的。

使用J2EE容器的安全框架比较适合一些中大型项目,在这些项目中,数据量以及访问量都比较大,运行的J2EE服务器可能达到几十台甚至几百台,而且运行内容都千差万别。如何在这样一个复杂的分布式系统中实现一个统一的用户安全管理机制?使用J2EE的JAAS以及LDAP支持的单点登录才能满足这样的大型应用。

在这样统一的安全框架中,以角色为分界线,用户、访问权限以及被访问的资源都可以最大限度的实现灵活配置和变动,彼此都不影响对方,各自都有无限的扩展能力,实现了一种统一资源的动态权限访问。

J2EE容器安全配置步骤小结如下:

(1)     立用户数据库,保存用户的口令和角色名称。

(2)     web.xml或ejb.xml中配置角色的访问权限,确定登录验证方式,是表单式还是基本的HTTP式。

(3)     配置容器的安全域,这和具体J2EE服务器有关,JBoss服务器是在Web层的jboss-web.xml或EJB层的jboss.xml中加入:

java:/jaas/SecurityRealm

其中,JdonSecurity需要在J2EE服务器中配置。

(4)     配置J2EE服务器,在JBoss的login-config.xml文件中加入JdonSecurity的安全策略。在JdonSecurity具体配置中,可以选择使用LoginModule,如使用LDAPLoginModule。如果用户的口令和角色数据保存在数据库中,选择DatabaseServerLoginModule,配置相应的查询语句,别忘记配置J2EE服务器的数据库源DataSource。

这是J2EE简单的容器安全配置,对付一般应用足够。在有些应用中,需要在用户登录Login后做一些全局布置之类的事情,那么此时就要实现Login.jsp。在Login.jsp中调用J2EE的安全服务,或者直接调用直接定制的LoginModule。这样可以实现定制性很强的统一的用户安全管理机制。

EJB的消息Bean(MDB)是除Session Bean以外的又一重要技术,EJB的消息Bean(MDB)允许开发者利用已有的在EJB技术方面的投资,将这些投资整合到一个异步消息传递的上下文环境中,使用JMS客户机发送一条消息给一个MDB。

在实际应用的架构设计中,异步处理机制和EJB的Session Bean等这些同步机制一样重要,不少程序员为了提高系统处理性能,一味地使用同步机制,并且试图一直加快这种同步处理速度,其实一个复杂系统的性能提高和单个功能的处理速度提高并无太多直接关系,一个复杂系统的性能提升往往和这个复杂系统的运行效率有关。

这种情况非常类似现实生活中的交通行驶。虽然道路上行驶的单个车辆本身能够开得很快,但是一旦发生交通堵塞,最终行驶速度很低。尤其是这种单件个体数量增多时。因此,复杂系统的运行效率是系统架构性能设计首要考虑的因素,需要检查系统中哪些环节会发生堵塞(Block),从而会导致整个系统的性能急剧下降。

运用异步处理机制是处理这种堵塞现象的一种有效办法,将那些容易发生堵塞现象的环节使用异步机制处理,从而提高整个系统的运行效率。

例如,本章中介绍的发送邮件功能是属于容易发生堵塞的一种现象。邮件接受服务器繁忙时,可能无法接受发送给它的邮件,因此必须多次向这个服务器发送,这种过程可能延续几秒甚至几个小时,如果让系统的其他功能等待发送功能完成后再执行,就犹如宝马车开在拖拉机后面一样。

异步处理机制还有另外一个主要优点:能够确保数据的可靠性,不丢失。在股票交易等重要系统应用中,如果股票服务器类似邮件服务器那样,因为服务器繁忙而拒绝接受用户买卖股票的指令,其后果是不堪设想和可笑的。因此,股票服务器的第一步是必须确保能够接受用户指令,并且保证这种接受数据不丢失,通过JMS的集群方案可以很好地解决这个问题。