跳轉到內容

使用 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());
    }
}
華夏公益教科書