在本系列上一篇文章 kintone 外掛開發 ⑥ 透過外掛 proxy 執行外部請求 - 入門概述篇 中,我們已介紹了 kintone 外掛 proxy 的概念與 API 用法。
這篇文章將透過一個實作範例,示範如何將 身份驗證資訊 安全地保存在 Plugin Proxy Config 中,並在前端程式碼中透過外掛 proxy 呼叫 API,同時避免敏感資訊暴露在瀏覽器端。
假設我們要在應用程式中實作一個 自動編碼 功能:
yymmdd
)。250808-0001
。在這個應用中,記錄具有權限限制(例如:只有建立者與審核者可以查看),如果直接用 Session 驗證 呼叫 REST API,會因權限不足而無法查詢到「正確的最後一筆記錄」,導致編碼計算錯誤。
為了能跨越權限限制,我們需要透過 API 權杖 取得資料。然而,若將 API 權杖直接寫在客製化程式碼中,就會暴露在前端的 JavaScript 內,任何人只要打開瀏覽器開發者工具就能看見。
為了解決這個問題,我們可以將客製化功能改寫為外掛,並且透過以下方式來隱藏驗證資訊:
setProxyConfig
)。kintone.plugin.app.proxy()
時,自動合併並帶入已保存的權杖。💡 外掛專案的建立與基本架構,請參考本系列 ① ~ ④ 篇:
本範例會搭配以下兩個套件,分別用於外掛設定畫面與前端客製化程式碼中:本範例使用以下套件:
kintone UI Component:
用於外掛設定畫面,快速建立輸入框、按鈕等 UI 元件,並保持與 kintone 原生介面風格一致。
CDN URL:
https://unpkg.com/kintone-ui-component/umd/kuc.min.js
Luxon:
用於前端客製化程式碼中處理日期與時間,例如取得當天日期、格式化為 yymmdd 前綴等。
CDN URL:
https://js.cybozu.com/luxon/3.7.1/luxon.min.js
在設定畫面中建立一個容器區塊,供 JavaScript 動態產生 UI 元件並插入:
<div id="plugin-setting-container"></div>
此程式負責外掛設定頁面的行為邏輯,提供管理者輸入 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 保存的設定如下:
https://{DOMAIN}.cybozu.com/k/v1/records.json
'GET'
{ 'X-Cybozu-API-Token': API_TOKEN }
{ app: 1 }
(假設應用程式ID為 1 )前端呼叫 kintone.plugin.app.proxy()
時帶入的參數:
https://{DOMAIN}.cybozu.com/k/v1/records.json?query={查詢條件}
'GET'
{}
{}
實際發送的請求(由系統自動合併):
https://{DOMAIN}.cybozu.com/k/v1/records.json?query={查詢條件}&app=1
'GET'
{ 'X-Cybozu-API-Token': API_TOKEN }
{}
⚠️ 注意:只有
setProxyConfig()
在 GET/DELETE 方法下,會自動將data
轉為 Query String。kintone.plugin.app.proxy()
本身並不會進行這個轉換。
實際在應用程式中執行自動編碼功能,透過 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()
呼叫 APIkintone.plugin.app.proxy(pluginId, url, method, headers, data, successCallback, failureCallback)
此方法會在符合相同 應用程式、外掛 ID、HTTP 方法、URL 前綴 等條件時,自動將 setProxyConfig()
預先儲存的 headers
與 data
合併到請求中,並由 kintone 伺服器發送實際的 API 請求,再將結果回傳至前端。
由於整個請求是在伺服器端完成,前端無法直接看到完整的請求內容,因此能有效隱藏 API 權杖等敏感資訊。
在產生請求網址時,先以 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()
保存的資訊相符,因此系統會自動:
data
(此處為 app: 應用程式ID
)轉換成 Query String,並附加到請求網址末端。這次的範例帶大家體驗了 Plugin Proxy 的操作流程,也實際展現了它在 kintone 環境中保護敏感資訊的效果。無論是呼叫外部 API,或是在 kintone 內部系統中跨權限查詢資料,都能透過這個方式安全地傳遞驗證資訊,降低憑證外洩的風險。在開發需要處理高權限或敏感資料的功能時,不妨考慮套用這個模式,讓功能實現與安全性兼顧。