iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

不會寫程式,我照樣做效能測試 - Claude Code × k6 的 30 天系列 第 10

Day 10|關聯(上):登入拿 token,為什麼這麼難

  • 分享至 

  • xImage
  •  

一、關聯:效能測試的經典難關,先從一個手環說起

想像一場音樂祭:售票口驗完票,在你手腕綁上今天的手環,之後每個攤位只看手環、不重新驗票。手環每天換花色——昨天的今天無效。

https://ithelp.ithome.com.tw/upload/images/20260908/20161809Dmk90JnhTv.png
圖 1:token 就是手環——錄下昨天的影片照著重播,攤位一看就拒收

網站的登入機制就是這一套:登入(售票口)發給你一個 token(手環),之後每個 API 請求(攤位)帶著它通行。這解釋了為什麼傳統「錄製回放」的思維在這裡必然失敗:你錄下的請求裡裝著錄製當下的那個 token,重播時它早已失效。腳本必須改成「每次執行都先去領新手環」——從登入回應中撈出動態值、帶進下一步,這個動作就叫關聯(correlation)。在錄製回放工具的年代,關聯是要手工從回應裡摳字串的苦活,也是效能測試最著名的勸退關卡;腳本化加上 AI 協作讓它親民了許多,但觀念你必須自己懂——因為要撈哪個值、帶去哪裡,是你對系統的理解,不是工具的。

除了 token,常見的動態值還有:session 識別碼、CSRF 防護碼、一次性表單代碼、帶簽名的時間戳。共同特徵:每次都不同、下一步一定用得到。看到這類值,腦中就要亮起「關聯」兩個字。

二、登入的三種常見形態:先認得出,才開得出需求

實務上的登入九成落在三種形態。判斷方法都一樣:開 DevTools 的 Network,看登入那一筆請求的回應。

https://ithelp.ithome.com.tw/upload/images/20260908/20161809loGQAXY5Pa.png
圖 2:三種形態的辨識特徵與 k6 應對——認形態是你的工作,寫程式是 AI 的工作

• 形態一(JSON API+token):登入回應是 JSON、裡面有 token 欄位。現代前後端分離的系統多半是這種——今天的實作主角
• 形態二(表單+cookie):登入回應的 headers 有 Set-Cookie。傳統網站與許多內部系統是這種。好消息:k6 內建 cookie jar,收到的 cookie 會自動保存、後續請求自動帶上——腳本幾乎什麼都不用做,只要按順序先做登入那一步
• 形態三(OAuth):文件出現 client_id、client_secret、token endpoint 這些詞,企業與第三方 API 常見。流程是先對 token 端點換一個 access_token,之後以「Bearer 開頭」的 header 帶入。憑證一律走 Day 8 的環境變數

你的分工從此清楚:打開 DevTools 認出形態,把形態與觀察到的欄位講給 Claude Code,程式碼交給它。認不出來也沒關係——把登入回應的樣子(記得抹掉真實 token)貼給它,請它判斷是哪一種。

三、動手做:完整登入流——註冊、登入、撈 token、帶著走

QuickPizza 提供完整的使用者 API,我們直接做最完整的版本:setup 裡註冊一個唯一的測試帳號(Day 9 的唯一值馬上派上用場)、登入、撈出 token,所有 VU 帶著它訪問需要授權的評分 API。

Prompt 1|生成完整登入流腳本

請建立 login-flow.js,流程如下:
1. setup():先 POST https://quickpizza.grafana.com/api/users
   註冊一個唯一帳號(username 用時間戳組成的假信箱,password 自訂),
   再 POST /api/users/token/login 登入,從回應 JSON 撈出 token 回傳。
   兩步都要 check 狀態碼(註冊 201、登入 200)。
2. default(data):帶著 token(Authorization: Token 開頭)
   GET /api/ratings,check 狀態碼 200。
3. 3 個 VU、30 秒、間隔 1 秒。
請逐段解釋,特別是「token 是怎麼從登入回應被撈出來、又怎麼被帶到下一步的」。

生成的核心段落會像這樣——關聯發生的那兩行,用註解特別標出:

const BASE = 'https://quickpizza.grafana.com';
 
export function setup() {
  // Day 9 的唯一值:每次執行都是全新帳號,不會互相干擾
  const username = `perf_${Date.now()}@test.example.com`;
  const password = 'Perf_12345678';
 
  const reg = http.post(`${BASE}/api/users`,
    JSON.stringify({ username, password }),
    { headers: { 'Content-Type': 'application/json' } });
  check(reg, { '帳號建立成功(201)': (r) => r.status === 201 });
 
  const login = http.post(`${BASE}/api/users/token/login`,
    JSON.stringify({ username, password }),
    { headers: { 'Content-Type': 'application/json' } });
  check(login, { '登入成功(200)': (r) => r.status === 200 });
 
  const token = login.json('token');  // ★ 關聯:從回應撈出動態值
  return { token };                   //   交給每一輪使用
}
 
export default function (data) {
  const res = http.get(`${BASE}/api/ratings`, {
    headers: { Authorization: `Token ${data.token}` },  // ★ 帶進下一步
  });
  check(res, { '帶 token 可以進門(200)': (r) => r.status === 200 });
  sleep(1);
}

跑起來 checks 應該全綠。注意這支腳本的密碼直接寫在裡面——因為這是拋棄式的練習帳號,用完即棄;公司系統的帳密永遠走環境變數,兩者的界線要分明。

驗證實驗:把手環拆掉,確認門真的有在看

Day 4 的老方法:驗證機制要先看過它「會擋人」。把 default 裡的 Authorization 那行整行註解掉(行首加 //),再跑一次:

  ✗ 帶 token 可以進門(200)
    ↳  0% — ✓ 0 / ✗ 87

這時去看 http_req_failed,會發現它也同步飆高——回應是 401(未授權)。這個實驗一石三鳥:確認 API 真的有在驗 token、認識了 401 這個之後除錯常見的狀態碼、也再次練習了「分辨系統壞掉與腳本問題」——現在的失敗是你自己造成的,系統其實運作得完全正確。把註解拆掉,恢復全綠再收工。

四、共用 token,還是各自登入?

剛才的腳本把登入放在 setup——整個測試只登入一次,所有 VU 共用那個 token。這是模式 A。另一個選擇是把登入搬進 default,每個 VU 每輪自己登入,這是模式 B。哪個對?取決於你這次要觀測什麼:

https://ithelp.ithome.com.tw/upload/images/20260908/20161809E2GyF5WYSD.png
圖 3:模式 A 省負載但不真實,模式 B 真實但登入系統也被壓——先問這次要觀測什麼

• 測 API 本身的效能、登入不是主角:模式 A。乾淨、省事,登入系統不會被順帶壓到
• 登入本身就是要測的對象(例如開賣瞬間全員登入),或需要最貼近真實:模式 B——而且帳號要夠多,Day 9 的參數化直接接上

還記得 Day 9 的快取假象嗎?模式 A 有個隱藏版:所有請求用同一個 token,伺服器端的授權驗證可能被快取——授權這一段的成本被你測不到了。多數情況無傷大雅,但如果授權邏輯本身很重(查權限表、打外部服務),這就是選模式 B 的另一個理由。把「這次觀測什麼」想清楚再選模式——這一分鐘的思考,就是測試設計。

五、注意事項:登入測試的紅線與細節

https://ithelp.ithome.com.tw/upload/images/20260908/20161809RSdSzBOF0C.png

給 RD 的一句話:QA 要壓的系統若掛在公司 SSO 後面,最好的支援是提供「測試環境專用的驗證旁路」——一組不走 MFA 的測試帳號區段,或測試模式的 token 簽發端點。沒有這個,QA 只剩兩條路:用個人帳號(資安惡夢),或放棄測登入後的功能(覆蓋惡夢)。

六、觀念驗證:三個問題確認你有帶走今天的重點

• 同事把三個月前錄的 HTTP 請求存檔重播,全部 401——用手環比喻解釋給他聽,並說出「關聯」是哪兩個動作。(第一節)
• 要測「週一早上九點全公司同時登入內部系統」——該用模式 A 還是 B?帳號怎麼準備?哪個系統要先打招呼?(第四節、第五節)
• 測試跑到第 35 分鐘,checks 從全綠瞬間變全紅、錯誤都是 401——最可能的原因是什麼?怎麼事先避免?(第五節)

七、小結

今天把效能測試最著名的難關拆開了:關聯就是「撈出動態值、帶進下一步」兩個動作,手環比喻解釋了錄製回放為什麼必然失敗。三種登入形態的辨識法讓你在 DevTools 前就能開出正確需求;完整登入流的實作串起了 Day 9 的唯一值與 setup;「拆手環」實驗則確認了門真的有在看。最後是那個一分鐘的測試設計判斷:共用 token 還是各自登入,取決於這次要觀測什麼。

必須誠實預告:今天的流程在 QuickPizza 上一次就通,因為它是為教學設計的示範站。真實系統的第一次,幾乎都不會這麼順——token 藏在 cookie 裡、多一層 CSRF、回應結構跟文件不一樣。明天整篇就處理這件事:腳本壞掉了,怎麼帶著 AI 把它修好。

附錄:三種形態的 k6 對應速查

// 形態一:JSON token(本篇主示範)
const token = loginRes.json('token');
http.get(url, { headers: { Authorization: `Token ${token}` } });
// 有些系統用 Bearer 開頭:Authorization: `Bearer ${token}`
 
// 形態二:表單 + cookie(k6 自動處理)
http.post(loginUrl, { username: u, password: p });  // 表單格式直接給物件
http.get(protectedUrl);   // cookie 已自動帶上,不用寫任何東西
 
// 形態三:OAuth client credentials(示意,細節依供應商文件)
const res = http.post(tokenUrl, {
  grant_type: 'client_credentials',
  client_id: __ENV.CLIENT_ID,
  client_secret: __ENV.CLIENT_SECRET,
});
const accessToken = res.json('access_token');
http.get(apiUrl, { headers: { Authorization: `Bearer ${accessToken}` } });

上一篇
Day 9|測試資料的兩難:參數化,以及你製造的髒資料怎麼辦
下一篇
Day 11|關聯(下):腳本壞掉了,如何請 Claude Code 帶你除錯
系列文
不會寫程式,我照樣做效能測試 - Claude Code × k6 的 30 天11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言