iT邦幫忙

0

【kintone】CORS問題與 kintone.proxy 的使用

  • 分享至 

  • xImage
  •  

在現代的網頁開發中,跨來源資源共享(Cross-Origin Resource Sharing,簡稱 CORS)是一個經常被討論的議題。CORS 是一種瀏覽器的安全機制,用來防止來自不同來源的請求存取用戶敏感資訊。然而,這項安全性設計在實際應用中也為開發者帶來了不少挑戰,特別是在需要跨域整合第三方服務或 API 時。

kintone 提供了豐富的 API 功能,讓開發者能夠快速構建客製化解決方案。但在與外部服務整合時,CORS 問題往往成為一大障礙。為了解決這個問題,kintone 提供了 kintone.proxy() 的方法,讓開發者能夠透過 kintone 平台的代理功能來處理跨域請求,進一步提高開發靈活性和安全性。

本文將說明什麼是 CORS,以及如何透過 kintone.proxy() 解決跨域問題。

CORS(Cross-Origin Resource Sharing)

CORS 問題的核心在於瀏覽器的同源政策(Same-Origin Policy)。這是一種安全機制,用來限制網頁從一個來源(origin)請求另一個來源的資源,避免惡意網站竊取敏感資訊。

「同源」的定義是 URL 的協議域名埠號都必須一致。當一個網頁的來源不同於目標伺服器的來源時,瀏覽器會自動阻止請求,除非目標伺服器明確授權該跨域請求。

為什麼在 kintone 客製化代碼中會遇到 CORS 問題?

在 kintone 平台上撰寫客製化代碼時,這些代碼執行於使用者的瀏覽器前端。當我們需要從前端代碼直接發送 API 請求到第三方伺服器(如外部服務的 REST API)時,這些請求的來源是 kintone 的域名(例如 https://example.kintone.com),而目標伺服器的域名可能是另一個完全不同的來源(例如 https://api.example.com)。

由於這兩者的來源不同,瀏覽器會認為這是一個跨域請求,並且根據同源政策阻止請求的執行,從而產生 CORS 問題。如果目標伺服器未配置適當的 CORS 標頭來允許跨域請求,開發者在 kintone 的前端代碼中無法正常與該 API 進行通信。

舉例說明:

  1. 場景:在 kintone 的記錄詳細頁面中,你希望撰寫一段 JavaScript,將記錄中的數據發送到外部的數據分析服務。
  2. 行為:該 JavaScript 在瀏覽器中執行,發送 POST 請求到 https://api.analytics.com/upload
  3. 結果:因為 https://example.kintone.comhttps://api.analytics.com 是不同的來源,瀏覽器會阻止該請求,並在開發者工具中顯示 CORS 錯誤訊息。

在 kintone 的客製化代碼中發送 API 請求時,請求是直接從使用者的瀏覽器前端發出的,因此自然會受到瀏覽器同源政策的影響。解決這個問題需要伺服器正確設置 CORS 標頭,或者使用 kintone 提供的 kintone.proxy() 功能來代理請求,繞過這些限制。

kintone.proxy

kintone JavaScript API 中提供了 kintone.proxy() 的方法,透過 kintone 的代理伺服器發送請求至外部伺服器,來避開 CORS 問題。

函式

kintone.proxy(url, method, headers, data, successCallback, failureCallback)

引數

引數 型別 必須 說明
url 字串 必須 欲執行之外部 API Url
method 字串 必須 執行 API 使用之 http 方法,可指定以下值:GET, POST, PUT, DELETE
headers 物件 必須 欲攜帶之請求標頭 (headers),不指定內容時請傳入空物件 {}
data 物件 必須 欲攜帶之請求主體 (body),不指定內容時請傳入空物件 {}
successCallback 函式 可省略 當請求完成時執行的回呼函數
failureCallback 函式 可省略 當請求失敗時執行的回呼函數

successCallback 的引數會傳遞以下資訊:

  • 第一個引數:response body 回應主體(字串)
  • 第二個引數:status 狀態碼(數值)
  • 第三個引數:response headers 回應標頭(物件)

省略 successCallback 與 failureCallback 時,將返回一個 Promise 物件,若請求完成,該物件會解決(resolve)為一個包含回應主體、狀態碼和回應標頭的陣列;若請求失敗,該物件會以代理 API 的回應主體(字串)作為拒絕理由而被拒絕(rejected)。

使用 callback 寫法

kintone.proxy(
  'https://api.example.com',
  'GET',
  {},
  {},
  (body, status, headers) => {
    // success
    console.log(status, body, headers)
  },
  (error) => {
    // error
    console.log(error)
  }
)

使用 Promise (async/await) 寫法

try {
  const [body, status, headers] = await kintone.proxy(
    'https://api.example.com',
    'GET',
    {},
    {}
  )
  // success
  console.log(status, body, headers)
} catch (error) {
  // error
  console.log(error)
}

💡 推薦使用 async/await 寫法,程式碼易讀性較高,也方便處理錯誤。

回應主體的資料處理

大部分 API 回傳的資料格式多為 JSON 物件,但在 kintone.proxy 中,回應主體會被轉換為字串,如果要進行物件的操作,就必須先透過 JSON.parse() 方法將其轉換回物件。

範例程式碼

try {
  const [body, status, headers] = await kintone.proxy(
    'https://api.example.com',
    'GET',
    {},
    {}
  )
  // success
  const dataObject = JSON.parse(body)
  console.log(dataObject)
} catch (error) {
  // error
  console.log(error)
}

更多的細節以及使用上的限制請參閱 kintone.proxy 官方文件(英文版)

kintone.proxy.upload

上述提到的 kintone.proxy() 方法僅能使用於文字資料的處理,如果要夠過 kintone 代理傳送檔案至外部時,則需要使用 kintone.proxy.upload() 方式。

函式

kintone.proxy.upload(url, method, headers, data, successCallback, failureCallback)

引數

引數 型別 必須 說明
url 字串 必須 請求之 Url
method 字串 必須 http 方法,可指定以下值:POST, PUT
headers 物件 必須 請求標頭 (headers),不指定內容時請傳入空物件 {}
data 物件 必須 請求主體 (body),規定格式與限制請見下方說明
successCallback 函式 可省略 當請求完成時執行的回呼函數
failureCallback 函式 可省略 當請求失敗時執行的回呼函數

successCallbackfailureCallback 的運作方式與 kintone.proxy() 相同。

引數 data 的格式與限制

data 物件必須為以下格式:

{
  format: 'RAW', // 上傳之格式,只能指定為 'RAW'
  value: // 欲上傳之檔案
}
  • format: 上傳的格式,僅能指定為字串 'RAW'
  • value: 上傳之檔案,可以是 blob 類型(包含 file),檔案大小限制最大為 200MB。

🔗 kintone.proxy.upload 官方文件(英文版)

實際操作範例

這裡演示一個將 kintone 附件欄位內的圖檔上傳至 Imgur,並且將圖片網址更新回記錄欄位中的簡單範例。效果如下圖:

首先建立一個 kintone 應用程式,加入以下欄位:

  • file:附件欄位,用來上傳圖片
  • imgur_link:連結欄位,用來存放上傳到 Imgur 後的圖片網址
  • 空白欄位:用來放置上傳按鈕

先建立一個客製化上傳按鈕,放到空白欄位中。

(() => {
  'use strict'

  const SPACE_ELEMENT_ID = '<Space Element ID>'
  const IMGUR_CLIENT_ID = '<Your Client ID>'

  kintone.events.on('app.record.detail.show', event => {
    // 將客製化按鈕插入至空白欄
    const spaceEl = kintone.app.record.getSpaceElement(SPACE_ELEMENT_ID)
    const uploadButton = createButton('Upload')
    spaceEl.appendChild(uploadButton)

    return event
  })

  function createButton(name) {
    const button = document.createElement('button')
    button.className = 'kintone-btn-primary'
    button.textContent = name
    return button
  }
})()

接著附加一個點擊事件到按鈕上,讓使用者點擊該按鈕就可以觸發上傳功能。

在上傳到外部之前,首先要透過 kintone REST API 取得附件檔案。從 event.record 中可以取得附件欄位中的值,其值為一陣列,裡面包含代表上傳在欄位中的檔案物件,但它並不是真正的檔案,只是檔案的資訊,必須要拿 fileKey 的值透過 REST API 請求才能拿到檔案。

🔗 kintone API 文件 - 下載檔案

uploadButton.addEventListener('click', async () => {
  try {
    // 取得附件檔案
    const file = event.record.file.value[0] // 取得附件欄位中的第一個檔案
    const downloadRes = await fetch(
      `/k/v1/file.json?fileKey=${file.fileKey}`,
      {
        method: 'GET',
        headers: {
          'X-Requested-With': 'XMLHttpRequest'
        }
      }
    )
    const blob = await downloadRes.blob()
    
  } catch (error) {
    console.error(error)
    window.alert('發生錯誤')
  }
})

拿到圖檔的二進制資料後,就可以按照 kintone.proxy.upload 的寫法,將檔案上傳到外部伺服器。

const data = {
  format: 'RAW',
  value: blob
}

const [body, status, headers] = await kintone.proxy.upload(
  'https://api.imgur.com/3/image',
  'POST',
  {
    'Authorization': `Client-ID ${IMGUR_CLIENT_ID}`
  },
  data
)

const resp = {
  body: JSON.parse(body),
  status,
  headers
}

請求成功後,可以從 body.data.link 取得圖片的網址。由於透過 kintone proxy 回傳的 response body 會被轉為字串,所以要先 parse 成物件,後續才方便取得資料。實作時可以先將 resp.body 印出來觀察回傳的資料結構。

最後再用 API 將網址更新回這筆記錄當中:

await kintone.api(
  kintone.api.url('/k/v1/record.json'),
  'PUT',
  {
    app: kintone.app.getId(),
    id: event.record.$id.value,
    record: {
      imgur_link: { value: resp.body.data.link }
    }
  }
)
window.alert('上傳成功!')
window.location.reload()

結語

使用 kintone proxy 是一個可以簡單解決 CORS 問題的方式,但因為它本身的功能有一些限制,操作起來會有一些不直觀、不方便的地方。如果真的要處理比較複雜的請求,可能直接架設一個代理伺服器會更加方便。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言