用 Rails 串接藍新金流一點都不難!

By Yuchan・6月 25 2021・技術文章
用 Rails 串接藍新金流一點都不難!

線上購物已成為目前消費行為的主流趨勢,而商家為了擁有更多元的付款方式以及方便對帳及退款的後台管理,通常就會仰賴第三方金流服務。

目前台灣主流的金流商為綠界科技、紅陽科技及藍新科技,三者提供的付款功能除了基本的信用卡收退款外也都各自有不同的服務,因此商家可以根據自己的商業需求來決定金流商。

本篇文章將會以藍新金流多功能付款 (MPG) 中的信用卡及 ATM 付款來做示範,在開始前需要做一些小準備:

  • 一個 Rails App
  • 藍新金流的測試帳號,須包含 Hash Key、Hash IV 及商店代號
  • 藍新金流 API 文件(本篇以 MPG 服務為例)

如果都已經準備好了,那我們就進入正題吧 ─=≡Σ(((っ゚∀゚)っ

建立 Service

先在專案資料夾內建立 app/service/newebpay/mpg.rb,我們會將金流的邏輯主要都寫在 mpg.rb 這個檔案內,如果不知道什麼是 Service Object 可以參考 Rails 程式碼整理術(進階)

module Newebpay
  class Mpg
    def initialize
    end

    def test
     "Test success!"
    end
  end
end

rails console 測試一下,看起來是沒問題的!

> Newebpay::Mpg.new.test
=> "Test success!"

開始串接 API

要開始串接 API 最重要的一件事,就是看看手冊內的需求是什麼。

🧙 傳送門:Newebpay MPG 串接手冊

資料交換

在與第三方 API 溝通時,要先確認我們的 request 需要哪些資料以及如何傳送,而藍新也提供了相當清楚的資訊:

  1. 以「Form Post」方式傳送交易資料至藍新金流進行交易
  2. Post 參數內需包含 MerchantIDTradeInfoTradeShaVersion
  3. TradeInfo 則由複數參數組成
  4. TradeInfoTradeSha 這兩個參數需要進行加密的動作

參數設定

藍新金流 Post 參數 圖片來源:藍新科技 / Post 參數

透過上圖,其實除了 TradeInfo 外欄位資訊都相當單純,因此我們可以先主攻 TradeInfo 來看看該帶入哪些資訊!

在文件 TradeInfo 內含參數欄位 這個分類中,最重要的是要先找到「必填」的資訊有哪些,以及選擇要使用哪種支付服務,最後再來將這些資訊整理起來:

# TradeInfo 欄位

{
  # 這些是藍新在傳送參數時的必填欄位
  MerchantID: "MS12345567",
  MerchantOrderNo: "yuchan6666",
  TimeStamp: Time.now.to_i.to_s,
  RespondType: "JSON",
  Amt: 123,
  Version: "1.6",
  ItemDesc: "第一次串接就成功!",
  Email: "[email protected]",
  LoginType: 0
  # --------------------------
  # 將要選擇的付款服務加上參數
  CREDIT: 1,
  VACC: 1,
  # 即時付款完成後,以 form post 方式要導回的頁面
  ReturnURL: "商品訂單頁面"
  # 訂單完成後,以背景 post 回報訂單狀況
  NotifyURL: "處理訂單的網址"
}

我自己是以 NotifyURL 去處理訂單的更新資訊,並且由 ReturnURL 處理訂單成功或失敗的轉址。其實除了上面使用到的參數外,像交易限制秒數及繳費有效期限等等也蠻常使用的,使用者可以看看自己有哪些需求再適時加上。

好的,既然 TradeInfo 已經知道要哪些資訊了,那麼就來排排看要送 post 的資料會是怎樣的結構。確定好內容後,就可以開始開始思考我們的 service 怎麼寫了!

{
    MerchantID: "MS123456789",
    TradeInfo: { 
         MerchantID: "MS123456789",
         RespondType: "JSON",
         TimeStamp: "1400137200",
         Version: "1.6",
         # etc...
     },
    TradeSha: # TradeInfo 經 AES 加密後再 SHA256 加密,
    Version: "1.6"
}

開始動工 service

1. 參數處理

抓回我們一開始寫的 mpg.rb,可以將重複使用的固定資訊在 initialize 時就準備好,並且將 form post 要使用的參數放入 service 中。

module Newebpay
  class Mpg
    attr_accessor :info

    def initialize(params)
      @key = "hash key"
      @iv = "hash iv"
      @merchant_id = "merchant id"
      @info = {}  # 使用 attr_accessor 讓 info 方便存取
      set_info(params)
    end

    def form_info
      {
        MerchantID: @merchant_id
        TradeInfo: trade_info,
        TradeSha: trade_sha,
        Version: "1.6"
      }
    end

    private

    def trade_info
      # AES256 加密後的資訊
    end

    def trade_sha
      # SHA256 加密後的資訊
    end

    def set_info(order)  
      info[:MerchantID] = @merchant_id
      info[:MerchantOrderNo] = order.slug
      info[:Amt] = order.amount 
      info[:ItemDesc] = order.name 
      info[:Email] = order.email 
      info[:TimeStamp] = Time.now.to_i 
      info[:RespondType] = "JSON"
      info[:Version] = "1.6"
      info[:ReturnURL] = "https://...."
      info[:NotifyURL] = "https://...."
      info[:LoginType] = 0 
      info[:CREDIT] =  1,
      info[:VACC] = 1
    end

  end
end

2. 將 info 進行加密處理

這應該是最難的一關了,文件內其實沒寫得很清楚需要哪些步驟,很多關鍵還只藏在範例的程式碼內,對於沒接觸過 PHP 或 .net 的人來說就稍微頭痛了一點。因此整理了一份適合 Rails 的範本來使用。

1.將內容轉為 query string

def url_encoded_query_string
  URI.encode_www_form(info)
end
# => "MerchantID=MS12345567&TimeStamp=1624388594&RespondType=JSON..."

2.trade_info 加密

利用 query 化的 info、hash key 與 hash iv 來進行 AES256 資料加密

def trade_info
  aes_encode(url_encoded_query_string)
end

def aes_encode(string)
  cipher = OpenSSL::Cipher::AES256.new(:CBC)
  cipher.encrypt
  cipher.key = @key
  cipher.iv = @iv
  cipher.padding = 0
  padding_data = add_padding(string)
  encrypted = cipher.update(padding_data) + cipher.final
  encrypted.unpack('H*').first
end

def add_padding(data, block_size = 32)
  pad = block_size - (data.length % block_size)
  data + (pad.chr * pad)
end

3.trade_sha 加密

將 trade_info 加密後的結果再透過 SHA256 加密一次

def trade_sha
  sha256_encode(@key, @iv, trade_info)
end

def sha256_encode(key, iv, trade_info)
  encode_string = "HashKey=#{key}&#{trade_info}&HashIV=#{iv}"
  Digest::SHA256.hexdigest(encode_string).upcase
end

這樣差不多就大功告成了,實作的完整程式碼如下:

module Newebpay
  class Mpg
    attr_accessor :info

    def initialize(params)
      @key = "hash key"
      @iv = "hash iv"
      @merchant_id = "merchant id"
      @info = {}  # 使用 attr_accessor 讓 info 方便存取
      set_info(params)
    end

    def form_info
      {
        MerchantID: @merchant_id
        TradeInfo: trade_info,
        TradeSha: trade_sha,
        Version: "1.6"
      }
    end

    private

    def trade_info
      aes_encode(url_encoded_query_string)
    end

    def trade_sha
      sha256_encode(@key, @iv, trade_info)
    end

    def set_info(order)  
      info[:MerchantID] = @merchant_id
      info[:MerchantOrderNo] = order.slug
      info[:Amt] = order.amount 
      info[:ItemDesc] = order.name 
      info[:Email] = order.email 
      info[:TimeStamp] = Time.now.to_i 
      info[:RespondType] = "JSON"
      info[:Version] = "1.6"
      info[:ReturnURL] = "https://...."
      info[:NotifyURL] = "https://...."
      info[:LoginType] = 0 
      info[:CREDIT] =  1,
      info[:VACC] = 1
    end

    def url_encoded_query_string
      URI.encode_www_form(info)
    end

    def aes_encode(string)
      cipher = OpenSSL::Cipher::AES256.new(:CBC)
      cipher.encrypt
      cipher.key = @key
      cipher.iv = @iv
      cipher.padding = 0
      padding_data = add_padding(string)
      encrypted = cipher.update(padding_data) + cipher.final
      encrypted.unpack('H*').first
    end

    def add_padding(data, block_size = 32)
      pad = block_size - (data.length % block_size)
      data + (pad.chr * pad)
    end

    def sha256_encode(key, iv, trade_info)
      encode_string = "HashKey=#{key}&#{trade_info}&HashIV=#{iv}"
      Digest::SHA256.hexdigest(encode_string).upcase
    end
  end
end

只要成功呼叫這個 service 就能夠產出符合 API 需求的 requset(如下圖)
至於要怎麼送 form post 出去這種簡單問題,就交給大家自己解決了(ゝ∀・)b

class PaymentsController < ApplicationController
  def create
    # 要記得附上該筆訂單的資訊,才有辦法建立付款喔!
    order = Order.find(1)
    @form_info = Newebpay::Mpg.new(order).form_info
  end
end

如果沒碰到什麼問題,應該就能夠順利看到付款畫面了,可喜可賀!

付款完成後

付款完成錢錢進帳,就大功告ㄔㄥˊ...等等,還沒結束!
錢錢入帳的確是大事,但如果我們的系統都沒有紀錄,那麼訂單的管理可能會發生世紀大亂。

還記得我們當初有設定 ReturnURLNotifyURL 嗎?當付款完成後,藍新也會以 post 的形式回傳支付訊息給我們,但回傳回來的資訊也是經過加密的,所以還需要再處理回傳參數的解密

解密的範例藍新也有提供 PHP 版本,而本篇同樣整理了 Rails 能夠使用的範例,至於回傳參數有哪些、商家會需要哪些資料,都可以在藍新手冊找到喔!

以下為實作範例:

module Newebpay
  class MpgResponse
    # 使用 attr_reader 可以更方便取用這些資訊
    attr_reader :status, :message, :result, :order_no, :trans_no

    def initialize(params)
      @key = "your hash key"
      @iv = "your hash iv"

      response = decrypy(params)
      @status = response['Status']
      @message = response['Message']
      @result = response['Result']
      @order_no = @result["MerchantOrderNo"]
      @trans_no = @result["TradeNo"]
      # etc...
    end

    def success?
      status === 'SUCCESS'
    end

    private
      # AES 解密
      def decrypy(encrypted_data)
        encrypted_data = [encrypted_data].pack('H*')
        decipher = OpenSSL::Cipher::AES256.new(:CBC)
        decipher.decrypt
        decipher.padding = 0
        decipher.key = @key
        decipher.iv = @iv
        data = decipher.update(encrypted_data) + decipher.final
        raw_data = strippadding(data)
        JSON.parse(raw_data)
      end

      def strippadding(data)
        slast = data[-1].ord
        slastc = slast.chr
        string_match = /#{slastc}{#{slast}}/ =~ data
        if !string_match.nil?
          data[0, string_match]
        else
          false
        end
      end
  end
end
class PaymentsController < ApplicationController
  def notify_response
    response = Newebpay::MpgResponse.new(params[:TradeInfo])
  end
end

同樣也是沒意外就能夠取得回傳資訊來更新訂單資訊了!

只要了解每個環節該做什麼事,用 Rails 串藍新金流沒那麼難,但因為篇幅問題沒辦法實際帶大家把所有的功能寫完,如果有興趣可以自己將內容補完並且跑起來試試看。但需要注意因為這是範例,實際使用時部分比較隱私的資訊最好使用 ENV 保護起來才是比較正確的喔。

我有將這次的實作內容放在 GitHub 上,或是對此篇文章有任何回饋都歡迎來信與我討論!

最後最後,如果對 Ruby on Rails 有興趣的話,可以參考近期即將開課的 Ruby on Rails 實戰課程