iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

重寫一套比我還老的系統:21 歲的校園文字廣播系統系列 第 16 篇

Day 16|畫完登入頁,然後呢?該真的讓它登入了吧

  • 分享至 

  • xImage
  •  

所以,今天該來幹點正事了。

終於要串 API 了嗎?

什麼叫終於?我也就是昨天偷懶了一下好嗎?

我去不早說。

何意味?

總之,前幾天登入頁的 UI 已經差不多弄好了。

帳號可以填、密碼可以打、按鈕也可以按。

看起來一切都很正常。

除了按下登入之後——

什麼都不會發生。

那不就是不能登入嗎?

至少它長得很像可以登入。

有什麼用?

咳。

所以今天的目標也很簡單:

把前端跟後端真的接起來。

前面花了這麼多天搞 Session、Cookie、HttpOnly,甚至連登入 API 都已經在後端躺好了。

也是時候讓前端真的把帳號密碼送過去了。

別讓 Agent 通靈,讓 OpenAPI 幫你說話!

既然要串 API,第一件事自然就是——

把 API 丟給 Agent,然後叫它自己看。

?

不是,我是認真的。

現在前端跟後端是兩個獨立的專案。如果直接跟 Agent 說:

「幫我把登入 API 串起來。」

那它至少得先知道幾件事情:

  • API 在哪裡?
  • Request Body 要送什麼?
  • Response 會回什麼?
  • 登入成功之後 Session 怎麼處理?
  • 登入失敗又會拿到什麼 Status Code?

當然,我可以把這些東西全部手動寫進 Prompt。

但問題是——

FastAPI 明明就已經幫我寫好了。

FastAPI 本身會根據我們定義的 Route、Pydantic Schema 等資訊,自動產生一份 OpenAPI Specification(OpenAPI 規格)。

而我們平常看到的 /docs,也就是 Swagger UI,其實就是把這份規格拿來生出一個方便人類閱讀、測試 API 的介面。

換句話說,與其讓我重新跟 Agent 描述一次:

「登入是 POST,路徑是什麼什麼,然後 Body 裡面有什麼什麼……」

不如直接讓後端自己說。

所以你要把 Swagger UI 丟給它?

呃,也不用。

Swagger UI 是給人看的。

真正有意思的是它背後那份:

/openapi.json

這裡面才是整個 API 的機器可讀規格。

於是事情突然變得簡單很多。

後端負責定義 API,OpenAPI 負責描述 API,Agent 負責照著規格串 API。

至少理論上是這樣。

「理論上」?

……

你不要急。

我們的後端部署在 localhost:8000 上,所以我們先來訪問 localhost:8000/openapi.json 吧。

如果是給人看的話可以訪問 /docs。

{"openapi":"3.1.0","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/auth/login":{"post":......中間省略無數多字{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}

看不懂。

那你拿它幹嘛?

我看不懂沒關係。

Agent 看得懂就好。

因為這份 JSON 裡面其實已經描述了後端有哪些 Endpoint(端點)、接受哪些資料、預期會回傳哪些資料,以及規格中定義的 Response。

所以比起我自己重新描述一次 API,再祈禱中間沒有漏掉什麼,不如直接把這份 OpenAPI 規格交給 Agent。

然後告訴它:

「這是目前後端的 OpenAPI 規格。請根據這份規格與現有 Angular 專案,規劃登入 API 的串接方式,先不要修改程式碼。」

這樣至少第一步,我們不用讓 Agent 通靈。

所以接下來就能一鍵完成了?

理論上。

又來?

因為很不幸地——

API 看得懂,不代表串起來就一定能動。

https://ithelp.ithome.com.tw/upload/images/20260930/201820316EUzM4OrBI.png

看起來問題不大。

但現在還有一個非常現實的問題。

資料庫裡根本沒有帳號。

那你要登入誰?

不知道。

6。

咳,總不能為了測個登入,現在跑去做一套註冊系統吧。

所以我們先準備一份 Seed Data(種子資料),塞幾個開發環境會用到的帳號進去。

畢竟我們現在的目標只有一個——

先讓這顆登入按鈕真的能登入。

那就先塞點資料進去吧

Seed Data(種子資料),簡單來說,就是預先準備一批讓系統可以開始運作的資料。

像我們現在資料庫雖然 Schema 都建好了,但裡面連個「一般學生」都沒有,更別說可以拿來登入的帳號。

所以我讓 Agent 幫忙補了一個手動執行的 Seed:

python -m database.seed

預設會先建立幾筆開發環境需要的基礎資料:

群組
└── 320

職務
├── 一般學生
├── 班代表
├── 資訊股長
└── 教師

不過這些東西還是不能登入。

你擱這擱這呢?

你急什麼。

畢竟正式環境可不能執行個 Seed,就憑空冒出幾個知道密碼的帳號。

所以測試帳號另外放在 --dev-account:

python -m database.seed --dev-account

密碼則從環境變數讀取:

SEED_DEV_PASSWORD=********

這樣才會額外建立開發用的學生與教師帳號。

而且這個 Seed 可以重複執行。

已經存在的資料就直接跳過,不會因為我手滑又執行一次,就突然多出:

一般學生
一般學生
一般學生
一般學生

四個一般學生不是很正常嗎?

……

是職務。

總之,先把 Migration 跑到最新版:

alembic upgrade head

再把 Seed 塞進去:

python -m database.seed --dev-account

現在,資料庫裡終於真的有一個可以拿來登入的帳號了。

那我們終於可以回來做今天原本要做的事情——

登入。

https://ithelp.ithome.com.tw/upload/images/20260930/20182031SyYLYZjZ7u.png

成功!

就這?

對阿。

你搞了這麼多天,最後就多了一個「歡迎,teacher」?

欸你不要小看這幾個字。

雖然畫面上只有一個框框,但剛剛按下登入之後,背後其實已經繞了一大圈。

Angular 先把登入需要的資料送到 /auth/login,後端找到對應的帳號、驗證密碼,建立 Session,再把 Session Token 塞進 HttpOnly Cookie。

接著前端再呼叫 /auth/me,瀏覽器自動把 Cookie 帶回去,後端才終於告訴前端:

「對,這傢伙是 teacher。」

然後才有了現在看到的——

歡迎,teacher。

所以這個框框其實很貴?

含金量很高的好嗎。

而這幾個字的背後,其實不只跑了一個 Request。

先從前端開始看。

按下登入之後發生了什麼?

前幾天我們已經把登入頁畫好了,所以今天真正新增的東西,其實沒有想像中複雜。

我先把跟登入有關的 API 包成一個 AuthService:

login(request: LoginRequest): Observable<AccountProfile> {
  return this.http
    .post<void>('/auth/login', request, { withCredentials: true })
    .pipe(switchMap(() => this.me()));
}

me(): Observable<AccountProfile> {
  return this.http.get<AccountProfile>(
    '/auth/me',
    { withCredentials: true }
  );
}

第一眼看起來好像就是:

登入,結束。

但仔細看的話,這裡其實偷偷做了兩次 Request。

你登入一次打兩次 API?

對。

第一次是:

POST /auth/login

Angular 把我們剛才輸入的帳號、身分類型、職務和密碼送到後端。

FastAPI 找到對應帳號、驗證密碼,接著建立一筆 Session。

如果一切正常,Response 裡還會出現一個我們前幾天講到爛掉的東西:

Set-Cookie: __Host-session=...

不過 Angular 並不需要知道裡面那串 Token 是什麼。

甚至,它不應該知道。

因為我們前面已經把這顆 Cookie 設成了 HttpOnly。

Token 交給瀏覽器保管,前端 JavaScript 不碰它。

那你怎麼知道現在登入的是誰?

好問題。

這就是第二個 Request 出場的地方。

這個 Request 就藏在剛才那段程式的這裡:

.pipe(switchMap(() => this.me()));

這又是什麼魔法?

先不管 switchMap。

我們先看後面那個 this.me()。

me(): Observable<AccountProfile> {
  return this.http.get<AccountProfile>(
    '/auth/me',
    { withCredentials: true }
  );
}

當 /auth/login 成功之後,前端馬上又送出第二個 Request:

GET /auth/me

這次我們沒有再傳帳號,也沒有再傳密碼。

甚至連剛才那串 Session Token 都沒有自己塞進 Request。

那後端怎麼知道你是誰?

瀏覽器知道。

還記得剛才 /auth/login 回來的:

Set-Cookie: __Host-session=...

嗎?

瀏覽器收到之後,就已經把這顆 Cookie 保存起來了。

而我們在 Request 裡又設定了:

withCredentials: true

因此呼叫 /auth/me 時,符合 Cookie 規則的情況下,瀏覽器就會自動把 Session Cookie 一起帶回去。

後端拿到 Cookie 裡的 Token,再把它做 SHA-256 雜湊,拿雜湊後的結果去資料庫尋找還有效的 Session。

找到 Session 之後,就可以一路找到它屬於哪個 Account、掛在哪個 Position 和 Group,最後組成一份 AccountProfile 回來:

export interface AccountProfile {
  id: string;
  account: string;
  position_name: string;
  group_name: string;
  display_name: string | null;
  is_active: boolean;
  created_at: string;
  updated_at: string;
}

所以整個流程其實是:

POST /auth/login
       ↓
驗證帳號密碼
       ↓
建立 Session
       ↓
Set-Cookie
       ↓
瀏覽器保存 Cookie
       ↓
GET /auth/me
       ↓
瀏覽器帶上 Cookie
       ↓
後端找到 Session 與 Account
       ↓
回傳 AccountProfile

等等,所以登入 API 自己不告訴你「你是 teacher」?

沒錯。

/auth/login 在這裡負責的是:

「證明你是你,然後建立登入狀態。」

而 /auth/me 負責的是:

「既然你已經登入了,那告訴我你是誰。」

兩件事情拆開之後,之後其他頁面如果想知道目前登入的是誰,也不用重新登入一次,只要再問 /auth/me 就好了。

至於剛才那個:

switchMap(() => this.me())

做的事情其實也就很好理解了。

登入成功之後,接著去取得目前使用者資料,再把最後拿到的 AccountProfile 往後傳。

於是 Login Component 最後收到的就不是 Session Token,而是使用者資料:

next: (profile) => {
  this.welcomeName.set(
    profile.display_name?.trim() || profile.account
  );
  this.isSubmitting.set(false);
}

我們的開發帳號沒有設定 display_name,所以最後就退回使用 account。

也就是:

teacher

於是繞了這麼大一圈之後——

歡迎,teacher。

好,這框框好像真的有貴一點。

你終於懂了。


上一篇
Day 15|中秋連假結束了,今天來閒聊一下吧。
下一篇
Day 17|CORS?反向代理?這都是些什麼跟什麼啊???
系列文
重寫一套比我還老的系統:21 歲的校園文字廣播系統 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Steven
iT邦新手 5 級 ‧ 2026-09-30 23:53:14

好酷

我要留言

立即登入留言