在现代的网页开发中,跨来源资源共享(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 进行通信。

举例说明:

  • 场景:在 kintone 的记录详细页面中,你希望撰写一段 JavaScript,将记录中的数据发送到外部的数据分析服务。
  • 行为:该 JavaScript 在浏览器中执行,发送 POST 请求到 https://api.analytics.com/upload。
  • 结果:因为 https://example.kintone.com 和 https://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 函式 可省略 当请求失败时执行的回呼函数

    successCallback 与 failureCallback 的运作方式与 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 问题的方式,但因为它本身的功能有一些限制,操作起来会有一些不直观、不方便的地方。如果真的要处理比较复杂的请求,可能直接架设一个代理伺服器会更加方便。