基于容器的用户安全管理系统

  作者:板桥banq

上页

5  Web层的实现

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

5.1  用户资料管理

用户资料管理主要是实现新用户注册;用户资料查询修改;丢失密码查询等功能。这些功能都是通过上面章节介绍的接口框架对后台EJB进行操作,建立UserService作为ServiceProxy的实现,如下:
/**
 *  接口框架的ServiceProxy的具体应用实现者
 * <p>Copyright: Jdon.com Copyright (c) 2003</p>
 * <p>Company: 上海汲道计算机技术有限公司</p>
 * @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();


1
图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建立下列对应关系:
<event-mapping>
    <web-action-name>
     com.jdon.security.web.SaveSignUpAction
    </web-action-name>
    <service-proxy-class>
      com.jdon.security.service.UserService
    </service-proxy-class>
</event-mapping>
这样,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加入如下语句:
<security-role>
    <role-name>Admin</role-name>
</security-role>
<security-role>
    <role-name>User</role-name>
</security-role>
定义了角色名称,下一步需要进行访问控制权限的分配,希望管理员角色可以访问Web中admin路径下的所有JSP文件或其他任何资源,那么在web.xml中配置:
<security-constraint>
    <display-name>admin</display-name>
    <Web-resource-collection>
      <Web-resource-name>Admin Area</Web-resource-name>
      <!--  admin路径下所有资源  -->
      <url-pattern>/admin/*</url-pattern>
      <!--  protected路径下所有资源  -->
      <url-pattern>/account/auth/*</url-pattern>
    </Web-resource-collection>
    <auth-constraint>
<!--  定义角色为admin -->
      <role-name>admin</role-name>
    </auth-constraint>
    <user-data-constraint>
      <transport-guarantee>NONE</transport-guarantee>
    </user-data-constraint>
  </security-constraint>
这样,如果用户以网址http://localhost:8080/admin/访问时,容器将检验通过登录验证用户的角色,如果是admin角色,将会正常访问,否则会被拒绝。
希望一般注册用户user能够访问路径account/auth下任何资源,同时也可以设定角色admin拥有该目录下的资源访问权限。
<security-constraint>
    <display-name>User protected</display-name>
    <Web-resource-collection>
      <Web-resource-name> Protected Area </Web-resource-name>
      <!--  protected路径下所有资源  -->
      <url-pattern>/account/auth/*</url-pattern>
      <!--  auth路径下所有资源  这并不是对应一个实际路径
            对应Strutss中Action的path,说明以auth为路径的
            所有Action也只有角色user能够访问
        -->
      <url-pattern>/auth/*</url-pattern>
    </Web-resource-collection>
    <auth-constraint>
<!--  定义角色为user -->
      <role-name>user</role-name>
    </auth-constraint>
    <user-data-constraint>
      <transport-guarantee>NONE</transport-guarantee>
    </user-data-constraint>
  </security-constraint>
在Strutss-config.xml中有如下配置:

<action path="/saveSignUpAction" name="userForm"
        type="com.jdon.security.web.SaveSignUpAction"
        validate="true"  input="/account/signup.jsp"  scope="request"
        >
      <forward name="createOk" path="/account/auth/success.jsp" />
      <forward name="editOk" path="/account/auth/success.jsp" />
</action>
<action path="/auth/editSignUpAction" attribute="userForm"
      type="com.jdon.security.web.EditSignUpAction"
      validate="false" scope="request" >
      <forward name="success" path="/account/signup.jsp" />
</action>
根据web.xml中的配置,路径为/auth/ editSignUpAction的EditSignUpAction只有注册用户才可以访问,因为没有设置根路径“/*”的访问限制,所以SaveSignUpAction则是任何用户都可以访问。
完成了访问控制权限的设置,现在可以设置用户的登录方式了。用户的登录有两种方式,即基于HTTP的登录验证方式和表单登录验证方式。
基本登录方式:
<login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>register user</realm-name>
</login-config>
那么,当用户访问受限制资源时,J2EE容器将自动提示界面如图6-2所示,要求用户输入用户名和密码。
表单登录验证方式:
<login-config>
    <auth-method>FORM</auth-method>
    <realm-name>SecurityRealm</realm-name>
    <form-login-config>
      <form-login-page>/account/login.jsp</form-login-page>
      <form-error-page>/account/login_error.jsp</form-error-page>
    </form-login-config>
<login-config>
当用户访问受限制资源时,J2EE容器自动将/account/login.jsp推向浏览器界面,要求用户输入用户名和密码,如图6-3所示。
最后,为了使Web容器的安全机制激活,需要指定特定的LoginModule,这和具体J2EE服务器相关,在JBoss中,需要配置jboss=web.xml如下:
  <security-domain>java:/jaas/ JdonSecurity </security-domain>
这表示web层的安全域将使用名为JdonSecurity 的JAAS配置。关于JAAS配置JdonSecurity将在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文件,加入如下语句:
<local-tx-datasource>
    <jndi-name>SSODS</jndi-name>
    <connection-url>jdbc:mysql://localhost:3306/ssodb</connection-url>
    <driver-class>com.mysql.jdbc.Driver</driver-class>
    <user-name>banq</user-name>
    <password>1234</password>
  </local-tx-datasource> 
</datasources>
其中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文件:
<application-policy name = "SecurityRealm">
   <authentication>
   <login-module
     code="org.JBoss.security.auth.spi.DatabaseServerLoginModule"
     flag = "required">
       <module-option name = "dsJndiName">java:/SSODS</module-option>
       <module-option name = "principalsQuery">
          select password from user where userId=?
       </module-option>
       <module-option name = "rolesQuery">
          select role, 'Roles' from role where userId=?
       </module-option>
     </login-module>
   </authentication>
</application-policy>
在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中加入:
<security-domain>java:/jaas/SecurityRealm</security-domain>
这样,整个容器的安全管理框架就可以有机地联系在一起。

邮件服务的配置

本项目中使用了邮件发送组件来发送用户丢失的密码,在开发配置中,需要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服务器需要验证,那么必须加入用户名和密码,如下:
<attribute name="User">xxxx</attribute>
<attribute name="Password">xxxx</attribute>
<property name="mail.smtp.auth" value="true"/>
SMTP 服务器是属于操作系统方面的系统设置,在Linux可以使用SendMail作为SMTP服务器,在Windows平台上可以通过管理工具配置SMTP服务器。

部署和发布

建立.ear项目文件,将需要的组件如邮件发送组件打包进来,复制到JBoss/server/default/deploy目录下,如果没有任何错误即表示发布成功。
本项目相当多地使用了容器特性,因此整合测试显得很重要,而且可能比较麻烦,但是只要依据框架标准准确地完成本项目各个部件,运行调试时的问题还是可以基本克服。

小结

本章主要是讨论如何利用J2EE容器的安全框架来实现应用系统的安全管理功能,这些操作实现原理可以应用于任何需要安全机制的系统。
相比前面章节讨论的“简单的用户注册系统”,本章部分操作显得过于复杂,但是这些技术的选用都是依据不同应用要求作出的。
使用J2EE容器的安全框架比较适合一些中大型项目,在这些项目中,数据量以及访问量都比较大,运行的J2EE服务器可能达到几十台甚至几百台,而且运行内容都千差万别。如何在这样一个复杂的分布式系统中实现一个统一的用户安全管理机制?使用J2EE的JAAS以及LDAP支持的单点登录才能满足这样的大型应用。
在这样统一的安全框架中,以角色为分界线,用户、访问权限以及被访问的资源都可以最大限度的实现灵活配置和变动,彼此都不影响对方,各自都有无限的扩展能力,实现了一种统一资源的动态权限访问。
J2EE容器安全配置步骤小结如下:

  • 立用户数据库,保存用户的口令和角色名称。
  • web.xml或ejb.xml中配置角色的访问权限,确定登录验证方式,是表单式还是基本的HTTP式。
  • 配置容器的安全域,这和具体J2EE服务器有关,JBoss服务器是在Web层的jboss-web.xml或EJB层的jboss.xml中加入:

<security-domain>java:/jaas/SecurityRealm</security-domain>
其中,JdonSecurity需要在J2EE服务器中配置。

  • 配置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的集群方案可以很好地解决这个问题。

 

基于Spring Security和数据库的身份验证和授权

Spring security安全机制深入使用

首页