Grails 中的安全資料繫結

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

引言

Grails 框架為 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
    }
}

考慮在 Spring 應用程式上下文中配置了一個 TaxCalculator 例項以及一個 InvoiceHelper 例項。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 例項執行依賴注入,然後將 params 繫結到該例項上執行資料繫結。

由於事件的順序,如果 params 包含一個名為 "invoiceHelper.taxCalculator.taxRate" 的請求引數,那麼這段程式碼就會受到上面描述的同樣問題的影響。

在 Grails 2.0.2 和 Grails 1.3.8 中,事件順序發生了變化,因此先建立 Vendor 物件,然後對例項執行資料繫結,最後執行依賴注入。

透過這種事件順序,資料繫結不會有更改 Spring bean 屬性的風險,因為 Spring bean 是在資料繫結發生後才注入的。

對於 Grails 2.0.2 和 Grails 1.3.8 之前的版本,管理這個問題的簡單方法是這樣的

程式碼清單 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 社群所有即將發生的活動。

檢視全部