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 社区所有即将发生的活动。

查看全部