跳转到内容

使用 Click 框架进行 Java Web 应用程序开发/最佳实践

来自维基教科书,为开放世界提供开放书籍

本节讨论设计和构建 Click 应用程序的最佳实践。涵盖以下主题

为了应用程序安全,强烈建议您使用声明式 JEE Servlet 路径基于角色的安全模型。虽然 Click 页面提供了一个onSecurityCheck()方法来滚动您自己的程序化安全模型,但声明式 JEE 模型提供了许多优势。

这些优势包括

  • 它是一种行业标准模式,使开发和维护更容易。
  • 应用程序服务器通常提供多种与组织安全基础设施集成的方式,包括 LDAP 目录和关系数据库。
  • Servlet 安全模型支持用户将页面添加为书签。当用户稍后访问这些页面时,容器将自动对其进行身份验证,然后再允许他们访问资源。
  • 使用此安全模型,您可以使您的页面代码免受安全问题的影响。这使您的代码更易于重用,或者至少更容易编写。

如果您的应用程序具有非常细粒度或复杂的安全性要求,您可能需要结合使用 JEE 声明式安全模型和程序化安全模型来满足您的需求。在这些情况下,建议您将声明式安全用于粗粒度访问,并将程序化安全用于更细粒度访问控制。

声明式安全

[编辑 | 编辑源代码]

声明式 JEE Servlet 安全模型要求用户在访问安全资源之前进行身份验证并处于正确的角色中。相对于许多 JEE 规范,Servlet 安全模型出奇地简单。

例如,要保护管理页面,您需要在您的web.xml文件中添加一个安全约束。这要求用户在访问admin目录下的任何资源之前,必须处于 admin 角色中。

<security-constraint>
   <web-resource-collection>
      <web-resource-name>admin</web-resource-name>
      <url-pattern>/admin/*</url-pattern>
   </web-resource-collection>
   <auth-constraint>
      <role-name>admin</role-name>
   </auth-constraint>
</security-constraint>

应用程序用户角色在web.xml文件中定义为security-role元素。

<security-role>
   <role-name>admin</role-name>
</security-role>

Servlet 安全模型支持三种不同的身份验证方法

  • BASIC- 仅推荐用于安全性不重要的内部应用程序。这是最简单的身份验证方法,它只是向用户显示一个对话框,要求他们在访问安全资源之前进行身份验证。BASIC 方法相对不安全,因为用户名和密码以 Base64 编码字符串的形式发布到服务器。
  • DIGEST- 推荐用于安全性中等水平的内部应用程序。与 BASIC 身份验证一样,此方法只是向用户显示一个对话框,要求他们在访问安全资源之前进行身份验证。并非所有应用程序服务器都支持 DIGEST 身份验证,只有更新版本的 Apache Tomcat 支持此方法。
  • FORM- 推荐用于需要自定义登录页面的应用程序。对于需要高安全级别的应用程序,建议您在 HTTPS 上使用 FORM 方法。

身份验证方法在 <login-method> 元素中指定。例如,要使用 BASIC 身份验证方法,您需要指定

<login-config>
   <auth-method>BASIC</auth-method>
   <realm-name>Admin Realm</realm-name>
</login-config>

要使用 FORM 方法,您还需要指定登录页面的路径和登录错误页面

<login-config>
   <auth-method>FORM</auth-method>
   <realm-name>Secure Realm</realm-name>
   <form-login-config>
      <form-login-page>/login.htm</form-login-page>
      <form-error-page>/login.htm?auth-error=true</form-error-page>
   </form-login-config>
</login-config>

在您的 Clicklogin.htm页面中,您需要包含一个特殊的j_security_check表单,其中包含输入字段j_usernamej_password. 例如

#if ($request.getParameter("auth-error"))
<div style="margin-bottom:1em;margin-top:1em;color:red;">
  Invalid User Name or Password, please try again.<br/>
  Please ensure Caps Lock is off.
</div>
#end

<form method="POST" action="j_security_check" name="form">
<table border="0" style="margin-left:0.25em;">
 <tr>
   <td><label>User Name</label><font color="red">*</font></td>
   <td><input type="text" name="j_username" maxlength="20" style="width:150px;"/></td>
   <td>&nbsp;</td>
  </tr>
  <tr>
   <td><label>User Password</label><font color="red">*</font></td>
   <td><input type="password" name="j_password" maxlength="20" style="width:150px;"/></td>
   <td><input type="image" src="$context/images/login.png" title="Click to Login"/></td>
  </tr>
</table>
</form>

<script type="text/javascript">
  document.form.j_username.focus();
</script>

在使用基于 FORM 的身份验证时,不要在 Click 登录页面类中放置应用程序逻辑,因为此页面的作用仅仅是呈现登录表单。如果您尝试在登录页面类中放置导航逻辑,JEE 容器可能会简单地忽略它或抛出错误。

将所有这些放在一起,下面是一个web.xml代码片段,其中包含对 admin 路径和 user 路径下页面的安全约束。此配置使用 FORM 方法进行身份验证,并且还会将未经授权 (403) 的请求重定向到/not-authorized.htm页面。

<web-app>

    ..

    <error-page>
        <error-code>403</error-code>
        <location>/not-authorized.htm</location>
    </error-page>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>admin</web-resource-name>
            <url-pattern>/admin/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>user</web-resource-name>
            <url-pattern>/user/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
            <role-name>user</role-name>
        </auth-constraint>
    </security-constraint>

    <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>Secure Zone</realm-name>
        <form-login-config>
            <form-login-page>/login.htm</form-login-page>
            <form-error-page>/login.htm?auth-error=true</form-error-page>
        </form-login-config>
    </login-config>

    <security-role>
        <role-name>admin</role-name>
    </security-role>

    <security-role>
        <role-name>user</role-name>
    </security-role>

</web-app>

有关使用安全性的更多信息,请参见以下资源

包和类

[编辑 | 编辑源代码]

设计项目包结构的一个好方法是最初按技术对包进行分类。因此,在 Click 应用程序中,我们所有的页面都将包含在page包中。这与页面自动映射功能也非常匹配。

所有项目的域实体类都将包含在entity包中,服务类将包含在service目录中。请注意,entitypackage 的替代名称包括 domain 或 model。我们通常还有一个util包用于任何不适合其他包的杂散类。

按照惯例,Java 包名称是单数形式,因此我们有 util 包,而不是 utils 包。

下面说明了 MyCorp Web 应用程序的示例项目结构

Example Project Structure

在此示例应用程序中,我们使用声明式角色和基于路径的安全。中的所有页面adminpackage 和目录需要"admin"角色才能访问,而中的所有页面userpackage 和目录需要"user"角色才能访问。

页面类

[编辑 | 编辑源代码]

在开发应用程序页面类时,最佳实践是将通用方法放在基本页面类中。这通常用于提供对应用程序服务和日志记录器对象的访问方法。

例如,下面的 BasePage 提供对 Spring 配置的服务对象和 Log4J 日志记录器对象的访问

public class BasePage extends Page implements ApplicationContextAware 
{
    /** The Spring application context. */
    protected ApplicationContext applicationContext;

    /** The page Logger instance. */
    protected Logger logger;

    /**
     * Return the Spring configured Customer service.
     *
     * @return the Spring configured Customer service
     */
    public CustomerService getCustomerService() 
    {
        return (CustomerService) getBean("customerService");
    }

    /**
     * Return the Spring configured User service.
     *
     * @return the Spring configured User service
     */
    public UserService getUserService() 
    {
        return (UserService) getBean("userService");
    }

    /**
     * Return the page Logger instance.
     *
     * @return the page Logger instance
     */
    public Logger getLogger() 
    {
        if (logger == null) 
        {
            logger = Logger.getLogger(getClass());
        }
        return logger;
    }

    /**
     * @see ApplicationContextAware#setApplicationContext(ApplicationContext)
     */
    public void setApplicationContext(ApplicationContext applicationContext)  
    {
        this.applicationContext = applicationContext;
    }

    /**
     * Return the configured Spring Bean for the given name.
     *
     * @param beanName the configured name of the Java Bean
     * @return the configured Spring Bean for the given name
     */
    public Object getBean(String beanName) 
    {
        return applicationContext.getBean(beanName);
    }
}

应用程序通常使用边框模板,并且有一个BorderPage扩展BasePage并定义模板。例如

public class BorderPage extends BasePage 
{
    /** The root Menu item. */
    public Menu rootMenu = new Menu();

    /**
     * @see Page#getTemplate()
     */
    public String getTemplate()
    {
        return "/border-template.htm";
     }
}

大多数应用程序页面子类化BorderPage,除了 AJAX 页面,它们不需要 HTML 边框模板,通常扩展BasePage. 的BorderPage类不应包含通用逻辑,除了渲染边框模板所需的逻辑之外。通用页面逻辑应在BasePage类中定义。

为了防止这些基本页面类被自动映射,并成为直接可访问的网页,请确保没有与它们的类名匹配的页面模板。例如,上面的BorderPage类不会被自动映射到 border-template.htm

页面自动映射

[编辑 | 编辑源代码]

您应该使用 Click 页面自动映射配置功能。

自动映射将使您不必在click.xml文件中手动配置 URL 路径到页面类映射。如果您遵循此约定,维护和重构应用程序将非常容易。

您也可以快速确定页面 HTML 模板对应的 Page 类,反之亦然。如果您使用 ClickIDE Eclipse 插件,您可以通过按 Ctrl Alt S 在页面的类和模板之间切换。

示例click.xml自动映射配置如下(默认情况下启用自动映射)

<click-app>
  <pages package="com.mycorp.dashboard.page"/>
</click-app>

要查看页面模板如何映射到 Page 类,请将应用程序模式设置为调试启动时将列出映射。以下提供了 Click 启动列表的示例

[Click] [debug] automapped pages:
[Click] [debug] /category-tree.htm -> com.mycorp.dashboard.page.CategoryTree
[Click] [debug] /process-list.htm -> com.mycorp.dashboard.page.ProcessList
[Click] [debug] /user-list.htm -> com.mycorp.dashboard.page.UserList

当使用转发和重定向在页面之间导航时,您应该使用 Page 类而不是路径来引用目标页面。这为您提供了编译时检查,并且如果移动页面,您将不必更新 Java 代码中的路径字符串。

要使用 Page 类转发到另一个页面

public class CustomerListPage extends Page 
{
    public ActionLink customerLink = new ActionLink(this, "onCustomerClick");

    ..

    public boolean onCustomerClick() 
    {
        Integer id = customerLink.getValueInteger();
        Customer customer = getCustomerService().getCustomer(id);

        CustomerDetailPage customerDetailPage = (CustomerDetailPage)
            getContext().createPage(CustomerDetailPage.class);

        customerDetailPage.setCustomer(customer);
        setForward(customerDetailPage);

        return false;
    }
}

要使用 Page 类重定向到另一个页面,您可以从上下文中获取页面的路径。在下面的示例中,我们将客户 ID 作为请求参数传递给目标页面。

public class CustomerListPage extends Page
{
    public ActionLink customerLink = new ActionLink(this, "onCustomerClick");

    ..

    public boolean onCustomerClick() 
    {
        String id = customerLink.getValueInteger();

        String path = getContext().getPagePath(CustomerDetailPage.class);
        setRedirect(path   "?id="   id);

        return false;
    }
}

重定向到另一个页面的快速方法是简单地引用目标类。下面的示例通过使会话失效来注销用户,然后将他们重定向到应用程序主页。

public boolean onLogoutClick() 
{
   getContext().getSession().invalidate();

   setRedirect(HomePage.class);

   return false;
}

模板化

[编辑 | 编辑源代码]

使用页面模板化 - 强烈推荐。页面模板提供了许多优点,包括

  • 大大减少了您需要维护的 HTML 数量
  • 确保您的应用程序具有统一的外观和感觉
  • 使全局应用程序更改变得非常容易

对于许多应用程序来说,使用 Menu 控件来集中应用程序导航非常有用。菜单在WEB-INF/menu.xml文件中定义,该文件非常易于更改。

菜单通常在页面边框模板中定义,因此它们在整个应用程序中可用。Menu 控件不支持 HTML 渲染,因此您需要定义一个 Velocity 宏来以编程方式渲染菜单。您可以在边框模板中使用以下代码调用宏

#writeMenu($rootMenu)

使用宏渲染菜单的优点是,您可以在不同的应用程序中重用代码,并且要修改应用程序的菜单,您只需编辑WEB-INF/menu.xml文件即可。定义宏的最佳位置是 webroot/macro.vm文件,因为它被 Click 自动包含。

使用宏,您可以创建动态菜单行为,例如仅渲染用户有权访问的菜单项,使用isUserInRoles().

#if ($menu.isUserInRoles())
   ..
#end

您还可以使用 JavaScript 添加动态行为,例如下拉菜单。

日志记录

[编辑 | 编辑源代码]

对于页面日志记录,您应该使用 Log4j 库。另一个库是 Commons Logging。如果您使用的是 Commons Logging,请注意,此库在某些应用程序服务器上存在类加载器问题。如果您使用的是 Commons Logging,请确保您拥有最新版本。

定义记录器的最佳位置是在通用基页中,例如

public class BasePage extends Page 
{
    protected Logger logger;

    public Logger getLogger() 
    {
        if (logger == null) 
        {
            logger = Logger.getLogger(getClass());
        }
        return logger;
    }
}

使用此模式,所有应用程序基类都应扩展BasePage以便它们可以使用getLogger()方法。

public class CustomerListPage extends BasePage 
{
    public void onGet() 
    {
        try 
        {
            ..

        } 
        catch (Exception e) 
        {
            getLogger().error(e);
        }
    }
}

如果您有一些非常繁重的调试语句,您可能应该使用isDebugEnabled开关,以便在不需要调试时不调用它。

public class CustomerListPage extends BasePage 
{
    public void onGet() 
    {
        if (getLogger().isDebugEnabled()) 
        {
            String msg = ..

            getLogger().debug(msg);
        }

        ..
    }
}

请注意,Click 日志记录工具不是为应用程序使用而设计的,仅供 Click 内部使用。当 Click 在生产模式下运行时,它不会产生任何日志输出。

错误处理

[编辑 | 编辑源代码]

在 Click 中,未处理的错误将被定向到 ErrorPage 以供显示。如果应用程序需要额外的错误处理,它们可以在WEB-INF/click.xml. 例如

<pages package="com.mycorp.page" automapping="true"/>
  <page path="click/error.htm" classname="ErrorPage"/>
</pages>

中创建并注册自定义错误页面。通常,应用程序使用服务层代码或通过 servlet 过滤器来处理事务性错误,并且不需要在错误页面中包含错误处理逻辑。

自定义错误页面的潜在用途包括自定义日志记录。

例如,如果应用程序要求将未处理的错误记录到应用程序日志(而不是 System.out),则可以配置自定义 ErrorPage。示例ErrorPage错误日志记录页面如下所示

package com.mycorp.page.ErrorPage;
..

public class ErrorPage extends net.sf.click.util.ErrorPage 
{
    public void onDestroy() 
    {
    	Logger.getLogger(getClass()).error(getError());
    }
}
华夏公益教科书