GCP Cloud Run

Cloud Run

串接Line發送通知這種東西以前就想玩玩看了,最近剛好有個機會就來試試看了。我的使用情境大概是這樣:有個自動化的搶訂單網頁腳本,在取得訂單號碼後30分鐘內要匯訂金,完成後這張訂單才算成立,但人不可能隨時都在電腦前面顧著,這樣一點都不自動化。這時候用Line來發送記得匯訂金的通知就很方便了。


直接在前端JS呼叫Line BOT的API

經過詢問ChatGPT後,一開始給我的範例如下:

<script>

async function sendLineMessage(msg)
{ 
	const CHANNEL_ACCESS_TOKEN = "你的Channel Access Token"; // 長期有效 token
	const USER_ID = "Uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // 接收者的 User ID

    fetch("https://api.line.me/v2/bot/message/push", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${TOKEN}`
        },
        body: JSON.stringify({
            to: USER_ID,
            messages: [
                { type: "text", text: msg }
            ]
        })
    })
    .then(res => res.json())
    .then(data => {
        console.log("LINE API 回應:", data);
        alert("訊息已發送");
    })
    .catch(err => {
        console.error("發送失敗:", err);
    });

}
</script>
CORS

看起來雖然也是有模有樣的,但實際上一執行後,卻出現了CORS的問題。

會出現這個問題的原因是因為LINE Messaging API 不允許從瀏覽器直接呼叫(它沒開放 CORS),所以前端在 fetch() 時會被瀏覽器擋下來。但是Access-Control-Allow-Origin這個Header標頭要在後端伺服器端程式才可以設定,所以勢必要把程式搬到後端,並在header加上Access-Control-Allow-Origin。

順便附上Gemini的問答紀錄

問:

前端Javascript是不是沒有辦法直接設定"Access-Control-Allow-Origin"為*來避免CORS的問題

答:

您好,您提到的這個問題很常見,這是一個關於 CORS(跨域資源共享) 的經典迷思。

前端無法設定 Access-Control-Allow-Origin 的原因
Access-Control-Allow-Origin 這個 HTTP 標頭(Header)是伺服器端用來告訴瀏覽器,它願意讓哪些「來源」(Origin)的網頁來存取它的資源。

簡單來說:

當您在 A 網域的網頁上,透過 JavaScript 呼叫 B 網域的 API 時,瀏覽器會發出一個跨域請求。

這個時候,B 網域的伺服器需要在回覆的 Response Header 中,設定 Access-Control-Allow-Origin,並且它的值需要包含 A 網域,或者設定為 * 來允許所有來源。

如果伺服器沒有設定這個 Header,或者設定的值不包含您的網頁來源,瀏覽器就會基於安全政策,阻擋這個跨域請求,並在 Console 中拋出 CORS 錯誤。

因此,前端的 JavaScript 程式碼沒有權限去設定 Response Header。這是因為 HTTP Response Header 是由提供資源的伺服器所決定的,前端只能處理和發送 Request Header。

如何解決 CORS 問題?
既然前端無法自行解決,那麼解決 CORS 問題的根本方法,就是從伺服器端著手:

修改 API 伺服器設定:

這是最直接、最根本的方法。如果您有權限修改 API 伺服器的程式碼,只需在伺服器的 Response Header 中,加上 Access-Control-Allow-Origin: *。

但為了安全,更推薦的做法是只指定特定的來源,例如:Access-Control-Allow-Origin: https://your-frontend-domain.com。

使用代理伺服器(Proxy):

如果您無法修改 API 伺服器,或者出於安全考量不想直接開放 CORS,那麼可以自己建立一個代理伺服器。

前端的 JavaScript 程式碼向您自己的代理伺服器發送請求(這是同源請求,不會有 CORS 問題)。

代理伺服器收到請求後,再去向真正的 API 伺服器發送請求,取得資料後再轉發給前端。

由於伺服器之間的請求沒有同源策略的限制,所以可以順利拿到資料。

現在許多前端框架或工具(如 webpack-dev-server、Vite)都內建了代理伺服器的功能,可以讓您在開發時輕鬆設定,這是很常見的解決方案。

GCP Cloud Run

為了解決上面CORS的問題,但又覺得為了一個實驗性質的測試小API特地開一個.Net Core WebAPI專案,在部屬到遠端Server實在是有點懶。於是問了一下ChatGPT後,發現有GCP Cloud Run這種好東西。

GCP Cloud Run介紹

Cloud Run 是代管運算平台,能讓您在 Google 可擴充的基礎架構上直接執行容器。

只要能從程式碼建構容器映像檔,您就能在 Cloud Run 上部署以任何程式設計語言編寫的程式碼。事實上,建立容器映像檔是選用步驟。如果您使用的是 Go、Node.js、Python、Java、.NET Core、Ruby 或支援的架構,可以利用「以來源為基礎的部署」選項,透過所用語言的最佳做法來建構容器。

(source:什麼是 Cloud Run?)

 

GCP Cloud Run啟用流程

先到GCP的console頁面,在上方收尋欄位搜尋Cloud Run。

 

點選建立服務按鈕

 

我這邊懶人作法直接使用內嵌編輯器建立函示,並把地區選到台灣,其餘選項都用預設項目,好了之後點選建立。

 

可以看到下面的預設畫面,為一個線上編輯器

 

接著直接用Node.js在編輯器裡寫一個呼叫Line API的程式。這邊要注意函式進入點如果有把預設的helloHttp換掉的話,記得上下都要一致,不然到時候編譯會出錯的,程式編輯完成後點選儲存並重新佈署

 

原始碼

index.js

const functions = require('@google-cloud/functions-framework');
const fetch = require('node-fetch');

const TOKEN = ""; // 長期有效 token
const USER_ID = ""; // 接收者的 User ID

functions.http('sendLineMessage', async (req, res) => {

  // 設定 CORS
  res.set("Access-Control-Allow-Origin", "*");
  res.set("Access-Control-Allow-Methods", "POST, OPTIONS");
  res.set("Access-Control-Allow-Headers", "Content-Type");

  // 處理預檢請求
  if (req.method === "OPTIONS") {
    return res.status(204).send('');
  }

  // GET 測試路徑
  if (req.method === "GET") {
    return res.status(200).send("✅ LINE Proxy Service is running");
  }

  if (req.method === "POST") {
    try {
      const { message, to } = req.body || {};

      const r = await fetch("https://api.line.me/v2/bot/message/push", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${TOKEN}`,
        },
        body: JSON.stringify({
          to: to || USER_ID,
          messages: [{ type: "text", text: message || "No message" }],
        }),
      });

      const data = await r.json();
      return res.status(200).json(data);

    } catch (err) {
      console.error(err);
      return res.status(500).send("發送失敗" + err);
    }
  }

  // 其他方法
  res.status(405).send("Method Not Allowed");

});

package.json

{  
  "dependencies": {
    "@google-cloud/functions-framework": "^3.0.0",
    "node-fetch": "^2.6.7"
  }
}

API測試

部屬完成後,複製網址。

 

貼到API測試軟體測試看看

 

成功!


Ref: