使用 Grails 進行安全資料繫結

工程 | Jeff Scott Brown | 2012 年 3 月 28 日 | ...

簡介

Grails Framework 為 Web 應用程式開發人員提供了許多工具和技術,以簡化解決常見應用程式開發挑戰。

其中一些工具和技術簡化了通常與資料繫結相關的複雜且繁瑣的問題。一般而言,Grails 使資料繫結非常簡單,因為它提供了多種技術,可將資料地圖繫結到物件圖形。

應用程式開發人員務必了解每種技術的意涵,以便決定哪種技術對於任何給定的使用案例最合適且最安全。

Web 應用程式資料繫結概觀

許多 Web 應用程式的一個非常常見的任務是讓應用程式接受一組 HTTP 請求參數,並將這些參數繫結到一個物件。然後,該物件可能會儲存在資料庫中、用於執行某種類型的計算,或用於執行某種類型的應用程式邏輯。在 Grails 應用程式中,其中一些通常在控制器動作中執行,而資料通常繫結到網域物件。

考慮一個網域類別,它看起來像這樣

程式碼範例 1

class Employee {
    String firstName
    String lastName
    BigDecimal salary
}

應用程式中可能有一個表單,允許更新 firstName 和 lastName 屬性。該表單可能不允許更新 salary 屬性,該屬性可能僅由應用程式的其他部分更新。

用於更新特定員工的控制器動作可能看起來像這樣

程式碼範例 2

class EmployeeController {
    def updateEmployee() {
        // retrieve the employee from the database
        def employee = Employee.get(params.id)

        // update properties in the employee
        employee.firstName = params.firstName
        employee.lastName = params.lastName

        // update the database
        employee.save()
    }
}

Grails 可以透過允許類似以下的方式來簡化該過程

程式碼範例 3

class EmployeeController {
    def updateEmployee() {
        // retrieve the employee from the database
        def employee = Employee.get(params.id)

        // update properties in the employee
        employee.properties = params

        // update the database
        employee.save()
    }
}

這些範例都假設存在名為 firstName 和 lastName 的請求參數。在第一個範例中,我們針對每個需要更新的屬性都有一行程式碼,但在第二個範例中,我們只有 1 行程式碼,它涵蓋了所有需要更新的屬性。

在這個特定的範例中,我們只減少了 1 行程式碼,但如果 Employee 物件中有許多屬性需要更新,則第一個範例會變得更長且更繁瑣,而第二個範例則完全保持不變。

潛在問題

程式碼範例 3 比程式碼範例 2 更簡潔,需要的維護更少,但對於任何特定的使用案例而言,它可能不是最佳選擇。

更簡單方法的問題在於,它可能允許使用者更新應用程式開發人員不打算允許的屬性。

例如,如果存在名為 salary 的請求參數,則程式碼範例 2 中的程式碼會忽略該請求參數,但程式碼範例 3 中的程式碼會使用該參數的值來更新 Employee 物件中的 salary 屬性,這可能會造成問題。

應用程式程式碼可以使用多種技術來防禦類似情況。一種是使用程式碼範例 2 中顯示的方法。另一種是在要求執行資料繫結時,向 Grails 提供屬性名稱的白名單或黑名單。

以下顯示了一種提供白名單的方法

程式碼範例 4

class EmployeeController {
    def updateEmployee() {
        // retrieve the employee from the database
        def employee = Employee.get(params.id)

        // update the firstName and lastName properties in the employee
        employee.properties['firstName', 'lastName'] = params

        // update the database
        employee.save()
    }
}

程式碼範例 4 中的程式碼只會將 firstName 和 lastName 請求參數繫結到 employee 物件,而忽略所有其他請求參數。如果有名為 salary 的請求參數,它不會導致 employee 物件中的 salary 屬性被更新。

另一種技術是使用新增到所有 Grails 控制器的 bindData 方法。bindData 方法允許提供屬性名稱的白名單和/或黑名單

程式碼範例 5

class EmployeeController {
    def updateEmployee() {
        // retrieve the employee from the database
        def employee = Employee.get(params.id)

        // update the firstName and lastName properties in the employee
        bindData(employee, params, [include: ['firstName', 'lastName']])

        // or... bindData(employee, params, [exclude: ['salary']])

        // update the database
        employee.save()
    }
}

資料繫結和依賴注入

上述潛在問題可能會在許多方面對應用程式造成問題。其中一種情況是允許在應用程式中不打算允許的部分更新員工的 salary 屬性。另一個可能出現問題的方式是,如果對從 Spring 應用程式內容注入到物件中的任何屬性的物件執行資料繫結。

考慮如下程式碼

程式碼範例 6

class TaxCalculator {
    def taxRate

    def calculateTax(baseAmount) {
        baseAmount * taxRate
    }
}

class InvoiceHelper {
    def taxCalculator

    def calculateInvoice(...) {
        // do something with the parameters that involves invoking
        // taxCalculator.calculateTax(...) to generate some total
    }
}

假設 TaxCalculator 的實例與 InvoiceHelper 的實例一起在 Spring 應用程式內容中配置。TaxCalculator 實例自動連線到 InvoiceHelper 實例中。

現在考慮如下 Grails 網域類別

程式碼範例 7

class Vendor {
    def invoiceHelper
    String vendorName

    // ...
}

Grails 控制器可能會執行類似以下的操作來更新目前在資料庫中持續存在的 Vendor

程式碼範例 8

class VendorController {
    def updateVendor = {
        // retrieve the vendor from the database
        def vendor = Vendor.get(params.id)

        // update properties in the vendor
        vendor.properties = params

        // update the database
        vendor.save()
    }
}

這裡的一個潛在問題是,它可能會不經意地允許更新 Spring 應用程式內容中的 TaxCalculator 實例中的 taxRate 屬性。

如果有名為 invoiceHelper.taxCalculator.taxRate 的請求參數,則當執行 "vendor.properties = params" 時,這正是會發生的情況。根據應用程式中的其他一些詳細資訊,這可能會導致應用程式的行為出乎意料且有問題。

在 Grails 2.0.2 中,這不會成為問題,因為 Vendor 類別中的 invoiceHelper 屬性是動態型別的,並且如下所述,除非動態型別的屬性明確包含在白名單中,否則它們是不可繫結的。如果 invoiceHelper 屬性是靜態型別的,則它將受到資料繫結的約束。

在 Grails 2.0.2 之前,程式碼範例 8 中的程式碼是有問題的,但可以使用上述白名單或黑名單技術輕鬆解決。

當使用資料繫結建構子時,會出現相同問題的另一個版本

程式碼範例 9

class VendorController {
    def createVendor = {
        // create a new Vendor
        def vendor = new Vendor(params)

        // save to the database
        vendor.save()
    }
}

在 Grails 2.0.2 和 Grails 1.3.8 之前,當執行 "new Vendor(params)" 時,發生的情況是建立 Vendor 物件,然後再次對 Vendor 實例執行依賴注入,然後執行資料繫結,將參數繫結到實例。

由於事件的順序,如果參數包含名為 "invoiceHelper.taxCalculator.taxRate" 的請求參數,則此程式碼會遇到上述相同問題。

在 Grails 2.0.2 和 Grails 1.3.8 中,事件的順序已變更,因此建立 Vendor 物件,然後對實例執行資料繫結,然後執行依賴注入。

透過該事件序列,不會有資料繫結變更 Spring Bean 中的屬性的危險,因為 Spring Bean 要在資料繫結發生後才會注入。

對於 Grails 2.0.2 和 Grails 1.3.8 之前的 Grails 版本,管理此問題的簡單方法如下

程式碼範例 10

class VendorController {
    def createVendor = {
        // create a new Vendor
        def vendor = new Vendor()

        vendor.properties['vendorName'] = params

        // or... bindData(vendor, params, [include: ['vendorName']])
        // or... bindData(vendor, params, [exclude: ['invoiceHelper']])

        // save to the database
        vendor.save()
    }
}

這對於每個網域類別都不是問題,但對於具有自動連線到其中的 Spring Bean 的網域類別而言,這可能是問題。順便一提,同樣的一組問題也適用於 Grails 命令物件,它們也受到資料繫結和自動依賴注入的約束。

Grails 2.0.2 資料繫結改進

長期以來,Grails 一直支援這些技術。Grails 2.0.2 將包含更大的彈性來管理資料繫結。在 Grails 2.0.2 中,程式碼範例 4 和 5 中的程式碼的行為與先前版本完全相同。當提供白名單或黑名單時,它將受到尊重。

但是,當未提供白名單或黑名單時,例如在 "employee.properties = params" 中,Grails 2.0.2 的行為可能會有所不同,具體取決於 Employee 類別中的某些詳細資訊。

在 Grails 2.0.2 中,資料繫結機制預設會排除所有靜態、暫時或動態型別的屬性。為了更精細地控制預設可繫結和不可繫結的內容,Grails 2.0.2 支援新的 bindable 限制

程式碼範例 11

class Employee {
    String firstName
    String lastName
    BigDecimal salary

    static constraints = {
        salary bindable: false
    }
}

程式碼範例 11 顯示如何表示 salary 屬性預設為不可繫結。這表示當應用程式執行類似 "employee.properties = params" 的操作時,salary 屬性將不受資料繫結的約束。

如果屬性曾經明確包含在白名單中,例如 "employee.properties['firstName', 'lastName', 'salary'] = params",則它將受到資料繫結的約束。

結論

Grails 提供的資料繫結機制允許編寫簡潔、富有表現力的程式碼,而不會被大量繁瑣的資料繫結相關細節所困擾。應用程式開發人員務必了解使用這些技術的意涵,以便可以實作任何特定使用案例的最佳方法。

參考資料

取得 Spring 電子報

隨時掌握 Spring 電子報的最新資訊

訂閱

領先一步

VMware 提供訓練和認證,以加速您的進展。

深入了解

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

深入了解

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

檢視全部