iT邦幫忙

2

kintone 外掛開發 ⑦ 透過外掛 proxy 執行外部請求 - 實作範例篇

  • 分享至 

  • xImage
  •  

在本系列上一篇文章 kintone 外掛開發 ⑥ 透過外掛 proxy 執行外部請求 - 入門概述篇 中,我們已介紹了 kintone 外掛 proxy 的概念與 API 用法。
這篇文章將透過一個實作範例,示範如何將 身份驗證資訊 安全地保存在 Plugin Proxy Config 中,並在前端程式碼中透過外掛 proxy 呼叫 API,同時避免敏感資訊暴露在瀏覽器端。

情境說明

假設我們要在應用程式中實作一個 自動編碼 功能:

  • 編碼的前綴為記錄建立日期(格式:yymmdd)。
  • 後綴為四位數流水號,例如:250808-0001
  • 每天的流水號從 0001 開始遞增。
  • 保存編號的欄位為單行文字方塊,欄位名稱與代碼均為「申請單號」。

在這個應用中,記錄具有權限限制(例如:只有建立者與審核者可以查看),如果直接用 Session 驗證 呼叫 REST API,會因權限不足而無法查詢到「正確的最後一筆記錄」,導致編碼計算錯誤。

為了能跨越權限限制,我們需要透過 API 權杖 取得資料。然而,若將 API 權杖直接寫在客製化程式碼中,就會暴露在前端的 JavaScript 內,任何人只要打開瀏覽器開發者工具就能看見。

為了解決這個問題,我們可以將客製化功能改寫為外掛,並且透過以下方式來隱藏驗證資訊:

  1. 將 API 權杖保存在外掛的 Proxy 設定setProxyConfig)。
  2. 前端呼叫 kintone.plugin.app.proxy() 時,自動合併並帶入已保存的權杖。

💡 外掛專案的建立與基本架構,請參考本系列 ① ~ ④ 篇:

程式碼範例與解說

使用套件

本範例會搭配以下兩個套件,分別用於外掛設定畫面與前端客製化程式碼中:本範例使用以下套件:

  1. kintone UI Component
    用於外掛設定畫面,快速建立輸入框、按鈕等 UI 元件,並保持與 kintone 原生介面風格一致。

    CDN URL:

    https://unpkg.com/kintone-ui-component/umd/kuc.min.js
    
  2. Luxon
    用於前端客製化程式碼中處理日期與時間,例如取得當天日期、格式化為 yymmdd 前綴等。

    CDN URL:

    https://js.cybozu.com/luxon/3.7.1/luxon.min.js
    

外掛設定畫面(config.html)

在設定畫面中建立一個容器區塊,供 JavaScript 動態產生 UI 元件並插入:

<div id="plugin-setting-container"></div>

外掛設定畫面(config.js)

此程式負責外掛設定頁面的行為邏輯,提供管理者輸入 API 權杖,並將其安全地保存至 Plugin Proxy Config 中。

((PLUGIN_ID) => {
  'use strict'

  // 建立 UI 元件
  const tokenInput = new Kuc.Text({
    label: 'API權杖',
    value: ''
  })

  const saveButton = new Kuc.Button({
    text: '保存',
    type: 'submit'
  })
  const cancelButton = new Kuc.Button({
    text: '取消',
    type: 'normal'
  })

  const buttonGroup = document.createElement('div')
  buttonGroup.style.display = 'flex'
  buttonGroup.style.justifyContent = 'flex-end'
  buttonGroup.style.gap = '8px'
  buttonGroup.style.paddingTop = '16px'
  buttonGroup.style.marginTop = '60px'
  buttonGroup.style.borderTop = '1px solid #e4e4e4'
  buttonGroup.appendChild(saveButton)
  buttonGroup.appendChild(cancelButton)

  const container = document.querySelector('#plugin-setting-container')
  container.appendChild(tokenInput)
  container.appendChild(buttonGroup)

  // 保存外掛設定
  const apiUrl = kintone.api.url('/k/v1/records')
  saveButton.addEventListener('click', () => {
    // 將 API 權杖保存到 Plugin Proxy Config 的標頭中
    const proxyHeaders = { 'X-Cybozu-API-Token': tokenInput.value }

    // 將呼叫 REST API 時的參數 "app"(應用程式 ID)預先設定至 Plugin Proxy Config 中
    const proxyData = { app: kintone.app.getId() }

    // 呼叫 kintone.plugin.app.setProxyConfig 來保存設定
    kintone.plugin.app.setProxyConfig(apiUrl, 'GET', proxyHeaders, proxyData)
  })

  // 返回按鈕
  cancelButton.addEventListener('click', () => {
    window.location.href = '../../' + kintone.app.getId() + '/plugin/'
  })

  // 從 Plugin Proxy Config 中讀取已保存的 API 權杖
  const pluginProxyConfig = kintone.plugin.app.getProxyConfig(apiUrl, 'GET')
  const savedToken = pluginProxyConfig?.headers['X-Cybozu-API-Token'] || ''
  tokenInput.value = savedToken

})(kintone.$PLUGIN_ID)

程式碼重點說明

建立輸入元件

使用 kintone UI Component 建立文字輸入框(tokenInput)供輸入 API 權杖。

使用 kintone.plugin.app.setProxyConfig() 儲存設定
kintone.plugin.app.setProxyConfig(url, method, headers, data, successCallback)

註:data 參數為請求主體(body)內容。

在本範例中,setProxyConfig() 用來將 API 權杖保存到 Plugin Proxy Config 的 headers 中,同時將呼叫 REST API 所需的 app(應用程式 ID)預先存入 data,方便後續請求直接使用。
另外,透過 getProxyConfig() 可以取得已保存的請求設定,再從 headers 中讀取先前儲存的 API 權杖,並在使用者再次回到外掛設定頁面時,自動填入輸入框,免去重複輸入的麻煩。

⚠️ 注意:請勿將 API 權杖以 setConfig() 儲存,因為在前端可以透過 getConfig() 直接讀取內容,這樣就失去了使用 setProxyConfig() 隱藏敏感資訊的意義。

💡 關於 setProxyConfig 在 GET/DELETE 方法下的特殊行為

一般來說,GET 請求的參數應放在請求網址中,而不是請求主體(body)。
不過 setProxyConfig() 在方法為 GET 或 DELETE 時,會自動將 data 中的物件內容轉換為 Query String,並與前端呼叫 plugin.proxy() 時的網址合併。

以本範例來看,Plugin Proxy Config 保存的設定如下:

  • url:https://{DOMAIN}.cybozu.com/k/v1/records.json
  • method:'GET'
  • headers:{ 'X-Cybozu-API-Token': API_TOKEN }
  • data: { app: 1 } (假設應用程式ID為 1 )

前端呼叫 kintone.plugin.app.proxy() 時帶入的參數:

  • url:https://{DOMAIN}.cybozu.com/k/v1/records.json?query={查詢條件}
  • method:'GET'
  • headers:{}
  • data: {}

實際發送的請求(由系統自動合併):

  • url:https://{DOMAIN}.cybozu.com/k/v1/records.json?query={查詢條件}&app=1
  • method:'GET'
  • headers:{ 'X-Cybozu-API-Token': API_TOKEN }
  • data: {}

⚠️ 注意:只有 setProxyConfig() 在 GET/DELETE 方法下,會自動將 data 轉為 Query String。kintone.plugin.app.proxy() 本身並不會進行這個轉換。

前端客製化程式碼(customize.js)

實際在應用程式中執行自動編碼功能,透過 plugin.proxy() 使用預存的請求資料來呼叫 kintone REST API,來取得編碼所需的最新一筆記錄,並且完成編碼邏輯。

((PLUGIN_ID) => {
  'use strict'

  const { DateTime } = luxon
  const apiUrl = kintone.api.url('/k/v1/records')

  // 禁止編輯「申請單號」欄位
  kintone.events.on([
    'app.record.create.show', 'mobile.app.record.create.show',
    'app.record.edit.show', 'mobile.app.record.edit.show', 'app.record.index.edit.show'
  ], event => {
    const applyNumber = event.record['申請單號']
    if (applyNumber) {
      applyNumber.disabled = true
    }
    return event
  })

  // 新增並保存記錄時,透過 REST API 取得最後一筆資料,並自動產生「申請單號」
  kintone.events.on(['app.record.create.submit', 'mobile.app.record.create.submit'], async event => {
    const { record } = event
    const applyNumber = record['申請單號']

    try {
      // 取得當日的起始時間與結束時間
      const startTime = DateTime.now().startOf('day').toISO()
      const endTime = DateTime.now().endOf('day').toISO()

      // 查詢當日建立的最後一筆記錄
      const query = `建立時間 >= "${startTime}" and 建立時間 <= "${endTime}" order by $id desc limit 1`
      const reqUrl = `${apiUrl}?query=${encodeURIComponent(query)}`

      // 透過 Plugin Proxy 呼叫 REST API
      const [resBody, status, resHeader] = await kintone.plugin.app.proxy(PLUGIN_ID, reqUrl, 'GET', {}, {})
      const res = JSON.parse(resBody)
      if (status !== 200) throw res

      const lastRecord = res.records[0]

      // 產生申請單號
      const lastApplyNumber = lastRecord ? lastRecord['申請單號']?.value : null
      const prefix = DateTime.now().toFormat('yyMMdd')

      if (!lastApplyNumber) {
        applyNumber.value = `${prefix}-0001`
      } else {
        const lastNumber = lastApplyNumber.slice(-4)
        const nextNumber = String(Number(lastNumber) + 1).padStart(4, '0')
        applyNumber.value = `${prefix}-${nextNumber}`
      }

    } catch (error) {
      console.error(error)
      event.error = error.message
    }

    return event
  })

})(kintone.$PLUGIN_ID)

程式碼重點說明

使用 kintone.plugin.app.proxy() 呼叫 API
kintone.plugin.app.proxy(pluginId, url, method, headers, data, successCallback, failureCallback)

此方法會在符合相同 應用程式、外掛 ID、HTTP 方法、URL 前綴 等條件時,自動將 setProxyConfig() 預先儲存的 headersdata 合併到請求中,並由 kintone 伺服器發送實際的 API 請求,再將結果回傳至前端。
由於整個請求是在伺服器端完成,前端無法直接看到完整的請求內容,因此能有效隱藏 API 權杖等敏感資訊。

查詢條件與 Query String 的組成

在產生請求網址時,先以 REST API 的 query 參數指定篩選條件(本範例為「當日建立的最後一筆記錄」),並使用 encodeURIComponent() 進行編碼,以確保 URL 格式正確。
最終組成的請求網址為:

https://{DOMAIN}/k/v1/records.json?query={查詢條件}

由於呼叫 plugin.app.proxy() 時帶入的 Plugin ID、方法(GET)、以及 URL 前綴(https://{DOMAIN}/k/v1/records.json)與先前在外掛設定畫面中用 setProxyConfig() 保存的資訊相符,因此系統會自動:

  1. 將儲存在 Plugin Proxy Config 中的 API 權杖加入 headers。
  2. 將儲存在 Plugin Proxy Config 中的 data(此處為 app: 應用程式ID)轉換成 Query String,並附加到請求網址末端。

結語

這次的範例帶大家體驗了 Plugin Proxy 的操作流程,也實際展現了它在 kintone 環境中保護敏感資訊的效果。無論是呼叫外部 API,或是在 kintone 內部系統中跨權限查詢資料,都能透過這個方式安全地傳遞驗證資訊,降低憑證外洩的風險。在開發需要處理高權限或敏感資料的功能時,不妨考慮套用這個模式,讓功能實現與安全性兼顧。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言