指南
Callback 片段
Callback 片段(callback-snippet)
放在回調頁(例如 callback.html)。讀取 URL 中的 code,搭配登入片段存入的 code_verifier 換取 access token。
要點
redirect_uri必須與授權請求相同- 僅使用
client_id;client_secret 僅後端使用 - 成功後可將 token 存
sessionStorage供 Profile 等章節使用 - 若使用
embed=1(iframe/popup),則不會跳轉到redirect_uri,改由postMessage取得code
callback-snippet.html
<div id="hs-callback-status">處理登入回調…</div>
<script>
(async function () {
const API_BASE = "{{api_base}}";
const CLIENT_ID = "{{client_id}}";
const REDIRECT_URI = "{{web_origin}}/callback.html";
const VERIFIER_KEY = "hs_pkce_verifier";
const out = document.getElementById("hs-callback-status");
const params = new URLSearchParams(location.search);
const code = params.get("code");
const error = params.get("error");
if (error) {
out.textContent = "授權失敗: " + error;
return;
}
if (!code) {
out.textContent = "缺少授權碼";
return;
}
const verifier = sessionStorage.getItem(VERIFIER_KEY);
if (!verifier) {
out.textContent = "找不到 PKCE verifier,請重新登入";
return;
}
const res = await fetch(API_BASE + "/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
code_verifier: verifier,
client_id: CLIENT_ID,
}),
});
const data = await res.json();
if (!res.ok) {
out.textContent = "換 token 失敗: " + (data.error || res.status);
return;
}
sessionStorage.setItem("hs_access_token", data.access_token);
out.textContent = "登入成功,token 已儲存";
history.replaceState({}, "", location.pathname);
})();
</script>
embed=1:從 postMessage 取得 code(iframe / popup)
當授權 URL 加上 embed=1 時,授權完成會由託管頁(API 網域)向 parent/opener 發送:
{ type: "hsystem.oauth", code, state }{ type: "hsystem.oauth.error", error, state }
你應在 parent(或 opener)端監聽 message,並檢查 event.origin === new URL(API_BASE).origin 後再使用 code 換 token:
<script>
(function () {
const API_BASE = "{{api_base}}";
const CLIENT_ID = "{{client_id}}";
const REDIRECT_URI = "{{web_origin}}/callback.html";
const VERIFIER_KEY = "hs_pkce_verifier";
const apiOrigin = new URL(API_BASE).origin;
window.addEventListener("message", async function (e) {
if (e.origin !== apiOrigin) return;
const data = e.data || {};
if (data.type === "hsystem.oauth.error") {
console.error("授權失敗:", data.error, "state:", data.state);
return;
}
if (data.type !== "hsystem.oauth" || !data.code) return;
const verifier = sessionStorage.getItem(VERIFIER_KEY);
if (!verifier) {
console.error("找不到 PKCE verifier,請重新登入");
return;
}
const res = await fetch(API_BASE + "/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: data.code,
redirect_uri: REDIRECT_URI,
code_verifier: verifier,
client_id: CLIENT_ID,
}),
});
const token = await res.json();
if (!res.ok) {
console.error("換 token 失敗:", token.error || res.status);
return;
}
sessionStorage.setItem("hs_access_token", token.access_token);
console.log("登入成功,token 已儲存");
});
})();
</script>
