【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付

知曉雲發表於2019-03-25

【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付
本文主要側重於講述小程式線上支付功能中的程式設計思想和程式設計模式,並在必要的地方提供關鍵程式碼示例。(文末也將附上關鍵的 js 程式碼)

為方便演示,這裡將實現一個最簡單的虛擬商品的訂單支付功能,訂單略去了收貨地址和多規格、多數量的情況,示例中僅討論在商品詳情頁中直接建立訂單併發起支付的情況。需要分別定義 Product 表和 Order 表進行資料存取,在 BaaS 後臺中建立兩張資料表。

一、資料表結構設計

Product 表:

資料表錄入許可權:所有人

資料行讀寫許可權:建立者可寫,所有人可讀

【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付

Order 表:

資料表錄入許可權:所有人

資料行讀寫許可權:建立者可寫,建立者可讀

【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付

商品的訂單結算和支付流程一般包括“建立訂單 -> 支付 -> 更新訂單狀態”三個步驟。下文中將分析幾種實現該流程的方案,供我們一起探討。

二、客戶端建立訂單,客戶端更新訂單狀態

我們先來看下只在客戶端中如何處理這些邏輯。

1) 建立訂單:Order 表中建立一條新記錄,status 欄位預設值為 "no_paid",儲存訂單金額,商品快照和商品 id 以及訂單建立者,其中訂單建立者由 BaaS 的使用者系統自動處理,值為建立訂單的使用者 id:

/**
 * 建立訂單處理函式
 */
createOrderHandle() {
  const orderTableId = 12345678
  const tableObject = new wx.BaaS.TableObject(orderTableId)
  const createObject = tableObject.create()


  const product = this.data.product
  const data = {
    product_id: product.id,
    product_snapshot: product,
    total_cost: product.price,
    status: 'no_paid',
  }


  // 客戶端建立訂單,客戶端更新訂單狀態
  return createObject.set(data).save().then(res => {
    this.order = res.data || {}
    return this.pay(this.order)
  }).then(transactionNo => {
    return this.updateOrder(transactionNo)
  }).then(res => {
    wx.navigateTo({ url: '../order/order' })
  })


2)支付:呼叫 BaaS SDK 提供的支付方法 wx.BaaS.pay,調起微信支付:

/**
 * 發起微信支付
 * @param {Object} order
 */
pay(order) {
  const product = this.data.product
  const orderTableId = 12345678
  const params = {
    totalCost: order.total_cost,
    merchandiseDescription: product.title,
    merchandiseSchemaID: orderTableId,
    merchandiseRecordID: order.id,
    merchandiseSnapshot: product,
  }
  return wx.BaaS.pay(params).then(res => {
    return res.transaction_no
  })
}


3)更新訂單狀態:支付成功後,更新 status 欄位值為 "paid",並更新微信支付序列號:

/**
 * 更新訂單狀態
 * 僅在由客戶端更新訂單狀態時使用
 * @param {String} transaction_no 支付成功後由微信返回的微信支付序列號
 */
updateOrder(transaction_no) {
  const orderTableId = 12345678
  const tableObject = new wx.BaaS.TableObject(orderTableId)
  const recordId = this.order.id
  const record = tableObject.getWithoutData(recordId)


  record.set('status', 'paid')
  record.set('transaction_no', transaction_no)
  return record.update()
}

複製程式碼

我們從整體上來看支付流程,便能發現訂單狀態實質上是由客戶端中 updateOrder 方法發起請求來進行更新的。

而這一情況將導致極大的安全隱患。因為從原則上來說,我們認為來自客戶端的資訊都是不可信的,訂單狀態很容易被偽造出的一個請求跳過支付直接將狀態更新為 'paid',並更新一個假的 transaction_no。

這意味著,不花一分錢也能將訂單變為已支付。在生產環境中,任何情下都不應該使用這種支付流程。

三、客戶端建立訂單,觸發器更新訂單狀態

基於這種情況,你或許會想:既然由客戶端來更新訂單狀態會引起安全問題,又沒有後端開發者參與,要怎麼做?

BaaS 平臺中觸發器和雲函式可以幫你解決這個問題。它們可以完成這種非客戶端的處理邏輯,同時使用它們的時候跟開發後端應用又有很大的不同。

首先來看一下觸發器(Trigger),觸發器是一種當觸發條件被滿足,將會執行觸發器中的事先定義的動作,定義好的動作可以是運算元據庫或者呼叫雲函式。

我們希望當支付完成之後,觸發器可以幫我們自動地運算元據庫,更新訂單對應的 status 和 transaction_no 欄位。觸發器設定如下:

【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付
【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付
【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付

「觸發型別」選擇微信支付回撥,條件是支付成功後執行觸發器。一般觸發器型別常見的還有運算元據表,定時任務等,分別對應運算元據表後觸發和定時觸發。

「動作」定義了觸發器將要執行的操作,這裡是更新 Order 資料表對應的 status、total_cost 和 transaction_no 欄位。更多觸發器的具體細節,不同平臺的實現有所不同,在此不展開討論。

藉助觸發器,客戶端建立訂單成功後不需要再調 updateOrder 方法,Order 訂單的資料會自動更新成支付成功對應的狀態:

/**
 * 建立訂單處理函式
 */
createOrderHandle() {
  ... // 與上文相同
  
  // 客戶端建立訂單,觸發器自動更新訂單狀態
  return createObject.set(data).save().then(res => {
    this.order = res.data || {}
    return this.pay(this.order)
  }).then(res => {
    wx.navigateTo({ url: '../order/order' })
  })
}

複製程式碼

值得注意的是,上面介紹的第一種方案中 Order 表的 ACL 資料行讀寫許可權是建立者可寫的,意味著建立者可以對資料進行任意操作,將更新訂單狀態的工作交給觸發器後,Order 表的 ACL 資料行讀寫許可權應設定為「不可寫」,保證 Order 表的資料建立後不會由外部更改,提高了資料的安全性。

四、雲函式建立訂單,觸發器更新訂單狀態

細心的讀者可能發現了除了 status 和 transacton_no 欄位外,還由觸發器自動更新了 total_cost 欄位,儲存的是實際支付的金額。

這就引出了另外一個問題,雖然現在不能通過客戶端修改訂單狀態,但是建立訂單的所有資料仍是由客戶端發起請求,在請求引數中定義的,這種方式同樣很容易被人篡改資料,比如 1000 元的商品可以被更改成 1 元甚至 0 元,造成只需要花很少的錢就可以買到高價值的商品。

使用觸發器自動根據微信支付回撥更新 total_cost 可以保證無論何種情況下,資料中儲存的都是終端使用者實際支付的金額。雖然這種方式可以事後幫助我們發現訂單金額異常的問題,但還是不能解決在建立訂單時金額被篡改的問題,這又要如何解決呢?

這時候建立訂單的功能應該交給後端邏輯去做了,在 BaaS 平臺中就需要用到雲函式了,雲函式又被稱為 FaaS(Functions as a Service)函式即服務。

雲函式是一段可以部署在服務端的程式碼,關鍵詞是一段程式碼,而不是一整套的後端邏輯,它本質上就是函式而已,特別是對於執行在 node.js 環境下的雲函式來說,它跟平常所寫的 JavaScript 程式碼幾乎一模一樣,對前端開發者來說非常容易上手。雲函式可以由 SDK 或觸發器呼叫,也可以在雲函式之間相互呼叫。

為了避免建立訂單時客戶端資料篡改或商品資訊不能實時同步的問題,我們將建立訂單的邏輯遷移到 BaaS 平臺的雲函式中:

【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付

關注「知曉雲」微信公眾號,在微信後臺回覆「建立訂單」,獲取完整的【建立訂單】雲函式原始碼。

呼叫該雲函式時傳入商品 id,雲函式先查出此商品的具體資訊,再使用該商品資訊來建立訂單,整個過程在 BaaS 平臺的雲函式系統中完成,保證了資料的準確性。支付完成後,觸發器同樣會自動更新訂單狀態。客戶端中使用 invokeFunction 方法呼叫雲函式:

/**
 * 建立訂單處理函式
 */
createOrderHandle() {
  ... // 與上文相同
  
  // 使用雲函式建立訂單,觸發器更新訂單狀態
  wx.BaaS.invokeFunction('createOrder', {
    product_id: this.data.product.id
  }).then(res => {
    this.order = res.data || {}
    return this.pay(this.order)
  }).then(res => {
    wx.navigateTo({ url: '../order/order' })
  })
}

複製程式碼

由於建立訂單和更新訂單的操作已經分別交由雲函式和觸發器處理了,為了更好的安全性,Order 表的資料建立許可權和修改許可權都不應該對客戶端開放。

需要額外說明的是,而觸發器和雲函式系統級別的操作,相當於擁有最高許可權,所以我們這裡相當於禁止了客戶端中除了讀取資料外的所有操作,也就使得 Order 表的許可權控制和資料的準確性得到了安全的保障。

五、雲函式建立訂單,雲函式校驗並更新訂單狀態

我們再來研究一下程式碼,在 pay 這個方法中 wx.BaaS.pay(params) 所做的事情實際上是發起一個請求,獲取 BaaS 系統返回的支付解密資料,然後使用這些支付解密資料呼叫微信客戶端的支付功能,最終由使用者輸入密碼完成支付。

同理,根據客戶端提供的資料都不可信的原則,這個請求中 params 引數時的資料同樣可以被偽造,比如修改掉 totalCost 的值,也會導致最終支付的金額跟實際應該支付的金額不一值,根據之前觸發器的設定,雖然會如實地記錄了最終支付的金額,可以為後臺追溯金額異常的訂單提供依據,但是並不會阻止訂單更新為已支付的狀態。

當使用者支付成功後,我們更希望在更新訂單狀態前可以先進行支付資料的校驗,校驗不通過則不更新訂單狀態。想要實現這個功能,則要將觸發器和雲函式進行搭配使用了。

先將觸發器的動作型別改為雲函式:

【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付

微信支付成功後會觸發呼叫 verifyPayment 雲函式:

【實戰教程】只需三步,用雲函式又快又安全地實現小程式支付

客戶端的程式碼保持不變,此時整個流程是:呼叫 createOrder 雲函式建立訂單,拿到建立訂單成功的回撥資料後,發起支付,支付成功之後,由觸發器自動呼叫 verifyPayment 雲函式,校驗實付金額是否跟該商品的價格一致,若一致則更新該訂單為已支付狀態。

在 verifyPayment 雲函式中只考慮了校驗實付金額這一個維度,在實際開發中應綜合考慮更多維度來確保資料準確,在此不再展開討論。

至此,本文完成了一個小程式線上支付的案例,介紹瞭如何藉助 BaaS 平臺最快地實現小程式線上支付功能,通過開發過程中發現的各種安全問題,迭代出四種不同的實現方案,一步步完善支付功能的安全性,最後得出一個最快最安全實現小程式線上支付的方案

六、商品詳情頁和雲函式 js 程式碼

商品詳情頁 js 程式碼

/** 商品詳情頁 js 程式碼 **/
const productTableId = 12345678
const orderTableId = 123456789

Page({
  data: {
    product: {}
  },

  onLoad(options) {
    // 設定預設的商品 id,方便除錯
    const productId = options.id || '5ade97135acfb521865bf766'
    this.getProductDetail(productId)
  },
  /**
   * 獲取商品詳情資訊
   * @param {String} id
   */
  getProductDetail(id) {
    const tableObject = new wx.BaaS.TableObject(productTableId)
    const query = new wx.BaaS.Query()

    query.compare('id', '=', id)
    tableObject.setQuery(query).find().then(res => {
      const objects = res.data.objects || []
      const product = objects[0] || {}
      this.setData({ product })
    })
  },
  /**
   * 點選立即購買按鈕事件
   */
  createOrder(e) {
    wx.getSetting({
      success: res => {
        if (res.authSetting['scope.userInfo']) {
          this.createOrderHandle()
        } else {
          wx.BaaS.login()
        }
      }
    })
  },
  

  /**
   * 建立訂單處理函式
   */
  createOrderHandle() {
    const tableObject = new wx.BaaS.TableObject(orderTableId)
    const createObject = tableObject.create()

    const product = this.data.product
    const data = {
      product_id: product.id,
      product_snapshot: product,
      total_cost: product.price,
      status: 'no_paid',
    }
    
    // 客戶端建立訂單,客戶端更新訂單狀態
    // return createObject.set(data).save().then(res => {
    //   this.order = res.data || {}
    //   return this.pay(this.order)
    // }).then(transactionNo => {
    //   return this.updateOrder(transactionNo)
    // }).then(res => {
    //   wx.navigateTo({ url: '../order/order' })
    // })

    // 客戶端建立訂單,觸發器更新訂單狀態
    // return createObject.set(data).save().then(res => {
    //   this.order = res.data || {}
    //   return this.pay(this.order)
    // }).then(res => {
    //   wx.navigateTo({ url: '../order/order' })
    // })

    // 使用雲函式建立訂單,觸發器或雲函式更新訂單狀態
    wx.BaaS.invokeFunction('createOrder', {
      product_id: this.data.product.id
    }).then(res => {
      this.order = res.data || {}
      return this.pay(this.order)
    }).then(res => {
      wx.navigateTo({ url: '../order/order' })
    })
  },
  /**
   * 發起微信支付
   * @param {Object} order
   */
  pay(order) {
    const product = this.data.product
    const params = {
      totalCost: order.total_cost,
      merchandiseDescription: product.title,
      merchandiseSchemaID: orderTableId,
      merchandiseRecordID: order.id,
      merchandiseSnapshot: product,
    }
    return wx.BaaS.pay(params).then(res => {
      return res.transaction_no
    })
  },
  /**
   * 更新訂單狀態
   * @param {String} transaction_no 支付成功後返回的微信支付訂單號
   */
  updateOrder(transaction_no) {
    const tableObject = new wx.BaaS.TableObject(orderTableId)
    const recordId = this.order.id
    const record = tableObject.getWithoutData(recordId)

    record.set('status', 'paid')
    record.set('transaction_no', transaction_no)
    return record.update()
  }
})

複製程式碼

建立訂單雲函式

/** 建立訂單雲函式 **/
const productTableId = 12345678
const orderTableId = 123456789

exports.main = function createOrder(event, callback) {
  const {product_id} = event.data
  const user_id = event.request.user.id
  
  getProductDetail(product_id).then(product => {
    return createOrderHandel(product, user_id)
  }).then(res => {
    const order = res.data || {}
    callback(null, order)
  }).catch(err => {
    callback(err)
  })
}

function getProductDetail(id) {
  const tableObject = new BaaS.TableObject(productTableId)

  const query = new BaaS.Query()
  query.compare('id', '=', id)
  return tableObject.setQuery(query).find().then(res => {
    const objects = res.data.objects || []
    const product = objects[0] || {}
    return product
  })
}

function createOrderHandel(product, user_id) {
  const tableObject = new BaaS.TableObject(orderTableId)
  const createObject = tableObject.create()

  const data = {
    product_id: product.id,
    product_snapshot: product,
    total_cost: product.price,
    status: 'no_paid',
    created_by: user_id
  }
  return createObject.set(data).save()
}
複製程式碼

校驗並更新訂單狀態雲函式

/** 校驗並更新訂單狀態雲函式 **/
const productTableId = 12345678
const orderTableId = 123456789

exports.main = function verifyPayment(event, callback) {
  const data = event.data
  const totalCost = data.total_cost
  const orderId = data.merchandise_record_id
  const transactionNo = data.transaction_no
  const merchandiseSnapshot = data.merchandise_snapshot
  const productId = merchandiseSnapshot.id

  getProductDetail(productId).then(product => {
    if (product.price === totalCost) {
      updateOrder(orderId, transactionNo)
    }
  })
}

function getProductDetail(id) {
  const tableObject = new BaaS.TableObject(productTableId)

  const query = new BaaS.Query()
  query.compare('id', '=', id)
  return tableObject.setQuery(query).find().then(res => {
    const objects = res.data.objects || []
    const product = objects[0] || {}
    return product
  })
}

function updateOrder(orderId, transaction_no) {
  const tableObject = new BaaS.TableObject(orderTableId)
  const recordId = orderId
  const record = tableObject.getWithoutData(recordId)

  record.set('status', 'paid')
  record.set('transaction_no', transaction_no)
  return record.update()
}

複製程式碼

知曉雲是國內首家專注於小程式開發的後端雲服務。使用知曉雲,小程式開發快人一步。

相關文章