在现代的网页开发中,跨来源资源共享(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 的客制化代码中发送 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 问题的方式,但因为它本身的功能有一些限制,操作起来会有一些不直观、不方便的地方。如果真的要处理比较复杂的请求,可能直接架设一个代理伺服器会更加方便。