在你的網站上嵌入徽章驗證(不需 iframe,不需 SDK)

在你的網站中加入一小段 JavaScript 程式碼(或一次伺服器端 fetch),即可透過公開的 JSON-LD 端點驗證任何 Open Badge。沒有 iframe,沒有第三方追蹤器。

Nacho Coll 作者 更新於 16 分鐘閱讀
在你的網站中加入一小段 JavaScript 程式碼(或一次伺服器端 fetch),即可透過公開的 JSON-LD 端點驗證任何 Open Badge。沒有 iframe,沒有第三方追蹤器。

如果你簽發憑證,遲早會有人問:「我怎麼知道這枚徽章是真的?」誠實的回答絕不該是「相信這張 PNG 就好」。Open Badge v2.0 透過一個公開、機器可讀的驗證端點解決了這個問題,而在你自己的網站上放一個「已驗證」小工具,大約只需十行程式碼——不用 iframe,不用第三方嵌入腳本,也不用 SDK。

本文會確切示範如何呼叫該端點並呈現結果,包含原生 JavaScript,以及 Node 和 Python 的伺服器端寫法。我們也會說明「驗證」究竟檢查了什麼、這個端點與 badges.ninja 已經提供給獲頒者的面向使用者頁面之間的關係,以及那些讓驗證小工具看起來大功告成、實際上卻什麼都沒證明的常見錯誤。

「已驗證」究竟代表什麼

Open Badge v2.0 的斷言(assertion)是一份結構固定的 JSON-LD 文件:斷言本身(誰獲頒、何時獲頒)、對 BadgeClass 的參照(它是什麼、由誰定義標準),以及對簽發者資料的參照(誰為它背書)。當你「驗證」一枚徽章時,其實是在檢查三件不同的事:

  1. 斷言是否存在且可解析。 該頒發紀錄的 ID 是真實的,而不是亂猜或遭竄改的。
  2. 是否已被撤銷。 簽發者可以在事後撤銷徽章——例如誤發的憑證、違反政策、認證已過期——正確的驗證端必須把這點呈現出來,而不能只確認紀錄存在。
  3. 整條鏈是否一致。 斷言中對徽章和簽發者的參照,確實指向真實且彼此相符的紀錄,而不是孤立或不相符的 ID。

只請求某個 URL 並檢查是否回傳 200 狀態的驗證小工具,完全漏掉了第 2 點。已撤銷的斷言依然可以正常解析——只是在 payload 中帶有撤銷標記。如果你的小工具只檢查「請求是否成功」,已撤銷的憑證就會顯示為已驗證,這比沒有小工具還糟糕。

面向使用者的公開頁面 vs. API 端點

在 https://badges.ninja 上頒發的每一份獎項都有一個公開 URL,獲頒者可以直接分享,查看時不需要註冊:

公開憑證頁面

大多數獲頒者會把這個頁面連結到 LinkedIn、履歷或電子郵件簽名檔中——它是為人設計的,會顯示徽章圖片、簽發者和簽發日期,你不需要做任何事它就能運作。下方的 API 端點則以結構化 JSON-LD 的形式回傳同一份斷言,適合你在建構自己的介面、而不只是連結到我們頁面的時候使用。

端點

GET https://api.badges.ninja/certify-badge/award/{guid}

不需要驗證標頭——這是刻意設計成公開的,因為 Open Badges 的全部意義就在於任何人(雇主、招募人員、執照審查委員會)都能在沒有帳號的情況下驗證憑證。完整的介面規格,包括回傳斷言中的確切欄位名稱,請見驗證文件。

傳入獎項的 GUID 即可,你可以從自己的簽發紀錄中取得,也可以從獲頒者分享給你的 badges.ninja/awards/<guid> 連結中解析出來。

原生 JS:fetch 並呈現

這就是完整的小工具。不需要建置步驟,不需要 npm 套件,直接放進任何頁面即可:

<div id="badge-verify"></div>
<script>
  async function renderBadgeVerification(awardId, mountId) {
    const el = document.getElementById(mountId);
    el.textContent = "Checking badge status";
    try {
      const res = await fetch(
        `https://api.badges.ninja/certify-badge/award/${awardId}`
      );
      if (!res.ok) {
        el.textContent = "Credential not found.";
        return;
      }
      const assertion = await res.json();
      // —— revocation check first, a resolvable assertion isn't enough on its own
      if (assertion.revoked) {
        el.textContent = `Revoked: ${assertion.revocationReason || "no reason given"}`;
        return;
      }
      el.textContent = `Verified, issued ${new Date(assertion.issuedOn).toLocaleDateString()}`;
    } catch (err) {
      el.textContent = "Verification check failed.";
    }
  }
  renderBadgeVerification("REPLACE_WITH_AWARD_ID", "badge-verify");
</script>

把 REPLACE_WITH_AWARD_ID 換成真實的獎項 GUID。這就是全部的用戶端整合:沒有需要跟 CSS 搏鬥的 iframe 邊界,也沒有第三方腳本在驗證檢查之外附帶自己的分析或追蹤像素。

伺服器端:Node

如果你比較想在後端驗證——例如在你掌控的個人檔案頁面上顯示「認證合作夥伴」徽章之前,或是在入職流程中把某份憑證當作證明接受之前——fetch 的寫法完全相同,只是少了 DOM 的部分:

async function verifyAward(awardId) {
  const res = await fetch(
    `https://api.badges.ninja/certify-badge/award/${awardId}`
  );
  if (!res.ok) return { verified: false, reason: "not_found" };
  const assertion = await res.json();
  if (assertion.revoked) {
    return { verified: false, reason: assertion.revocationReason || "revoked" };
  }
  return { verified: true, issuedOn: assertion.issuedOn, badge: assertion.badge };
}

伺服器端:Python

import requests
# —— resolve the assertion, then check revocation before trusting it
def verify_award(award_id):
    res = requests.get(f"https://api.badges.ninja/certify-badge/award/{award_id}")
    if res.status_code != 200:
        return {"verified": False, "reason": "not_found"}
    assertion = res.json()
    if assertion.get("revoked"):
        return {"verified": False, "reason": assertion.get("revocationReason", "revoked")}
    return {"verified": True, "issued_on": assertion.get("issuedOn"), "badge": assertion.get("badge")}

兩個範例都把非 200 的回應視為「找不到」,而不是拋出例外。對公開的驗證端點來說,失效或隨意猜測的獎項 ID 是預期內的輸入,而不是例外狀況,所以應把它當作正常分支處理,而不是捕捉錯誤。

自己設計結果的樣式,而不是嵌入別人的介面

由於上面的小工具只是對你自己的 DOM 發出一次 fetch 呼叫,每一個像素都由你掌控。常見做法是依據回應直接對應出三種視覺狀態:

  • 已驗證——綠色勾號加上「已驗證」和簽發日期,樣式與你網站的徽章/標籤元件一致。
  • 已撤銷——使用低調或紅色的狀態,並顯示撤銷原因而不是隱藏它。獲頒者和第三方都能從誠實的「為什麼」中受益,而不是看到一次無聲的失敗。
  • 找不到——在視覺上與「已撤銷」同等處理(不要呈現虛假的成功狀態),但文案不同,因為對閱讀頁面的人來說,無效 ID 和刻意撤銷是兩種不同的情況。

這一切都不需要元件庫。對 verified / revoked / not_found 做一個 switch,再搭配三個 CSS 類別就夠了。

快取,避免頻繁請求端點

驗證端點是公開且免驗證的,這也代表不必擔心每次請求的點數成本——但如果在用戶端呼叫又不做快取,每次載入頁面仍然是一次網路往返。對於向每位訪客顯示同一枚徽章的頁面(例如頁尾的「認證合作夥伴」徽章),請在伺服器端以較短的 TTL 快取結果——五到十五分鐘就足夠了,因為撤銷並不是變化很快的訊號——而不是每次請求都重新取得:

const cache = new Map();
async function verifyAwardCached(awardId, ttlMs = 10 * 60 * 1000) {
  const hit = cache.get(awardId);
  if (hit && Date.now() - hit.at < ttlMs) return hit.result;
  const result = await verifyAward(awardId);
  cache.set(awardId, { result, at: Date.now() });
  return result;
}

對於依訪客而異的小工具(獲頒者在個人網站上嵌入自己的憑證),可以跳過快取,每次載入頁面時直接重新呼叫——在這種規模下,多一次請求可以忽略不計。

badges.ninja 已經為獲頒者提供了什麼

在建構完全自訂的嵌入之前,值得先了解現成可用的功能。每份獎項的公開頁面都包含現成的分享與嵌入程式碼片段,獲頒者可以直接複製,不需要自己寫任何程式碼:

附有社群按鈕與嵌入程式碼片段的公開分享頁面

如果你的使用情境是「讓獲頒者展示自己的徽章是真的」,那麼連結到他們的 badges.ninja/awards/<guid> 頁面往往比建構自訂小工具更簡單——它就是前面展示的面向使用者的頁面,並且已經替你處理好「已驗證」與「已撤銷」的區分。當你需要把驗證狀態呈現在你自己產品的介面內部時——例如合作夥伴目錄、內部稽核儀表板、核對應徵者所宣稱認證的招募工具——也就是跳轉到另一個網站並不是合適體驗的場景,再建構本文介紹的自訂小工具。

為什麼不用 iframe 或廠商的小工具?

有些平台會給你一段像 <iframe src="https://vendor.example.com/verify-widget"> 的程式碼來嵌入驗證狀態。它能用,但代價是實實在在的:你無法重新設計內容樣式來配合你的網站(只能接受廠商自帶的 CSS),它會引入一個你的 Content-Security-Policy 必須明確允許的跨來源文件,而且無論訪客是否在意驗證狀態,每次載入頁面都會向第三方的 iframe 主機發出請求。兩次呼叫的 fetch 小工具則避開了這三點——它是你的 DOM、你的樣式,而且只在你決定呼叫時才會執行。

走完完整的驗證鏈

如果你要建構比狀態小工具更嚴謹的東西——例如批次檢查憑證的稽核工具,或合規報告——就要走完整條鏈,而不是停在斷言這一步。斷言參照一個 badge URL(即 BadgeClass:名稱、描述、標準),而 BadgeClass 又參照一個 issuer URL(組織資料)。嚴謹的驗證端會把三者全部解析出來,並確認它們內部一致——徽章確實屬於它所宣稱的簽發者,斷言確實參照了它所宣稱的徽章——而不只是頂層斷言回傳 200。對於單一憑證的狀態小工具來說,這有些過深,但了解這種結構是值得的,萬一合作夥伴或稽核人員問起「你們的驗證到底有多深?」

需要避免的常見錯誤

  • 只檢查 HTTP 狀態。 如上所述,已撤銷的斷言依然可以解析。務必讀取 assertion.revoked,不要僅憑狀態碼就推斷驗證通過。
  • 在共用元件中寫死獎項 ID。 如果小工具要在許多獲頒者的頁面上重複使用,就應把獎項 ID 當作參數(data 屬性、查詢字串、prop)傳入,而不是寫死在腳本裡。
  • 悄悄吞掉網路錯誤。 如果 try/catch 什麼都不記錄,並且和真正不存在的徽章顯示同樣的「找不到」文字,之後除錯會非常痛苦。請在錯誤分支中區分「網路呼叫失敗」和「API 說不行」,至少在日誌裡區分。
  • 在用戶端應用程式中每次渲染都重新驗證。 如果小工具位於頻繁重新渲染的 React 或 Vue 元件內,應每次掛載驗證一次(或依上文快取部分所述,每個快取 TTL 視窗驗證一次),而不是每次重新渲染都驗證。

這會消耗點數嗎?免費方案能用嗎?

不會。驗證端點刻意放在管理簽發的點數體系之外——檢查徽章是否有效不會讓簽發者花費任何成本,也不要求呼叫方擁有帳號、API 金鑰或任何方案等級。無論徽章是在 Free、Starter 還是 Pro 方案下簽發的,都是如此:驗證是憑證本身的屬性,而不是受簽發者訂閱所限制的功能。這也是你可以放心把這個小工具放在流量難以預測的公開頁面(徵才頁面、合作夥伴目錄)上,而不必擔心與你帳號綁定的速率限制的原因——你呼叫的是唯讀的公開端點,而不是需要驗證的 API 路由。

有一點值得明確說明:該端點回傳的斷言 JSON 不包含獲頒者的原始電子郵件地址。獲頒者的電子郵件在後端是以雜湊形式儲存的,正是為了讓公開的驗證回應無法被爬取來取得個人聯絡資料——你拿到的資訊足以確認憑證(徽章、簽發者、日期、撤銷狀態),卻不會讓這個端點變成任何人遍歷獎項 ID 就能用來蒐集銷售名單的工具。

準備好簽發你的第一份可驗證憑證了嗎?

在 badges.ninja 免費開始——視覺化設計器、公開驗證頁面、PDF 證書、Open Badge v2.0 輸出。不需要信用卡。

如需了解斷言中每個欄位的完整註解結構,請參閱 JSON-LD 欄位參考。如果你是以程式方式簽發憑證,而不僅僅是驗證,API 快速入門涵蓋了驗證方式,以及產生本端點輸出內容的獎項建立呼叫。

Nacho Coll

關於作者

Founder & Engineer at Badges Ninja

Nacho founded Badges Ninja to make issuing verifiable digital credentials as simple as a single API call — Open Badge v2.0 badges and certificates, minted, hosted, and verifiable without standing up your own issuer infrastructure. Writes about the Open Badges spec, credential verification, and running a credentialing platform serverless on AWS, from the operator side of the wire.

返回部落格

相關文章