ASP.NET Core 與 JWT 認證筆記

1. AppDbContext 是什麼?

這是一個 Entity Framework Core (EF Core) 的資料庫上下文 (DbContext)。它的用途是:把資料表與 C# 類別 (Model) 做對應,讓你可以用 C# 物件來存取資料庫,而不用自己寫 SQL。

程式碼範例:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<User> Users { get; set; }
    public DbSet<Note> Notes { get; set; }
}
    

舉例:你可以寫 var users = await _context.Users.ToListAsync();,它會自動轉成 SQL:SELECT * FROM Users;

2. IConfiguration 是什麼?

IConfiguration 是 ASP.NET Core 內建的設定存取介面。它可以讀取 appsettings.json、環境變數、使用者祕密、Azure Key Vault 等等。

程式碼範例:

private readonly IConfiguration _config;
public UsersController(AppDbContext context, IConfiguration config)
{
    _context = context;
    _config = config;
}
    

假設你的 appsettings.json 有:

"Jwt": {
  "SecretKey": "12345678901234567890123456789012",
  "Issuer": "myApp",
  "Audience": "myUsers"
}
    

在程式裡你可以用 _config["Jwt:SecretKey"] 讀出字串 "12345678901234567890123456789012"。這就是你用來產生 JWT 的金鑰。

3. 409 Conflict 是什麼?

if (exists)
{
    return Conflict(ApiResponse<User>.Fail("Email 已被註冊"));
}
    

這裡的 Conflict(...) 代表 HTTP 狀態碼 409 Conflict。

HTTP 409 Conflict = 請求和伺服器目前的狀態有衝突。在這裡的意思是:「這個 Email 已經存在,新增會造成衝突。」

這比單純用 400 Bad Request 更精確,因為 400 表示「用戶輸入錯誤」,但 409 則是「用戶輸入沒錯,但和現有資料衝突」。

4. JWT 登入流程詳解

步驟 1:使用者登入

前端送出登入請求:

POST /api/users/login
Content-Type: application/json

{
  "email": "test@example.com",
  "password": "123456"
}
    

步驟 2:後端驗證帳號密碼

後端 (ASP.NET Core) 收到後做:

步驟 3:後端產生 JWT Token

假設資料:

(1) 建立 Header

{ "alg": "HS256", "typ": "JWT" }

Base64Url 編碼後:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

(2) 建立 Payload

{
  "sub": "abc-123",
  "email": "test@example.com",
  "exp": 1712345678
}
    

Base64Url 編碼後:eyJzdWIiOiJhYmMtMTIzIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNzEyMzQ1Njc4fQ

(3) 建立簽章

把兩段連起來:

data = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMtMTIzIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNzEyMzQ1Njc4fQ"

用 HMAC-SHA256 + secretKey 算出雜湊:

signature = HMACSHA256(data, "MySuperSecretKey")

結果再 Base64Url 編碼,假設:abc123xyz987

(4) 最終 Token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMtMTIzIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNzEyMzQ1Njc4fQ.abc123xyz987

步驟 4:後端回傳給前端

登入成功的 API 回應:

{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....abc123xyz987"
}
    

步驟 5:前端儲存 Token

前端(瀏覽器 / App)收到後,通常會存到:

例子 (前端 JS):localStorage.setItem("jwt", token);

步驟 6:前端帶著 Token 呼叫受保護 API

假設要呼叫「取得個人資料」的 API:

GET /api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....abc123xyz987
    

步驟 7:後端驗證 Token

後端做這些事:

  1. 拿到 Token,拆成三段 (Header, Payload, Signature)
  2. 用同樣的 secretKey 重新計算簽章
  3. 比對「算出來的簽章」 == 「Token 帶來的簽章」
  4. 如果一致,而且 exp 還沒過期 → Token 有效
  5. 從 Payload 讀出 sub = abc-123 → 知道是哪個使用者

步驟 8:後端回傳資料

假設 User = "test@example.com",回傳:

{
  "id": "abc-123",
  "email": "test@example.com",
  "name": "Test User"
}
    

5. HMAC-SHA256 雜湊說明

出來的是亂數麼?可以反解譯麼?

出來的不是亂數

HMAC-SHA256 是一種「雜湊演算法 (Hash)」,同樣的輸入 + 同樣的 secretKey,永遠會得到一樣的結果。

HMACSHA256("hello", "key") => abc123...
HMACSHA256("hello", "key") => abc123...  (一樣)
    

不能反解譯

雜湊是單向函數,就像「果汁機」:丟進去水果只能變成果汁,無法反推出原本的水果。

所以別人拿到簽章 (Signature) 這段字串,不能反推出 payload 或 secretKey。

安全性來源

因為只有伺服器知道 secretKey,攻擊者就算改了 Payload(例如把 "role":"user" 改成 "role":"admin"),也算不出正確的簽章,伺服器一驗證就會失敗。

6. JWT 驗證流程詳細步驟

假設 Token 是:aaa.bbb.ccc

後端驗證流程:

  1. 拆三段
    headerBase64 = aaa
    payloadBase64 = bbb
    signatureFromToken = ccc
                
  2. 重新算簽章
    expectedSignature = HMACSHA256(headerBase64 + "." + payloadBase64, secretKey)
  3. 比對簽章
    if (expectedSignature == signatureFromToken) {
        // Token 沒被竄改
    } else {
        // Token 無效 (可能被偽造)
    }
                
  4. 檢查過期時間

    解開 Payload(Base64 解碼 JSON):

    {
      "sub": "abc-123",
      "email": "test@example.com",
      "exp": 1712345678
    }
                

    檢查 exp 是否大於現在時間。如果過期 → Token 無效。

  5. 讀取使用者資訊

    如果驗證成功 → 從 Payload 取 sub = abc-123,就知道是哪個使用者。

7. 為什麼叫 JWT (JSON Web Token)?

所以組合起來就是:「一種基於 JSON 格式的 Web 授權憑證」。

8. 其他 Token 方式

1. Session Token (傳統方式)

2. Opaque Token (不透明 Token)

3. JWT (自包含 Token, Self-contained Token)

4. OAuth 2.0 / OpenID Connect Token

9. Redux 原理與資料存放

Redux 是什麼?

Redux 本質上是一個前端的狀態管理工具。「狀態」就是你應用程式裡的資料(例如:登入使用者資訊、購物車內容、通知數量…)。

Redux 的運作核心有三個要素:

  1. Store:就像一個大的「物件樹」,裡面存放應用程式的所有狀態。在 React 裡,通常只有一個 Store。
    {
      auth: { user: { id: 1, name: "Alice" }, token: "jwt..." },
      cart: { items: [ { id: 101, qty: 2 } ] }
    }
                
  2. Action:一個普通的 JS 物件,用來描述「發生了什麼事」。
    { type: "LOGIN_SUCCESS", payload: { id: 1, name: "Alice" } }
  3. Reducer:一個純函式 (pure function),負責接收舊的 state 和 action,產生新的 state。
    function authReducer(state, action) {
      switch (action.type) {
        case "LOGIN_SUCCESS":
          return { ...state, user: action.payload };
        default:
          return state;
      }
    }
                

整個過程就像一個「流水線」:使用者操作 → dispatch action → reducer 更新 state → UI 自動重新 render。

Redux 的資料存在哪裡?

為什麼每個 client 的資料都不一樣?

因為 Redux 的 state 是存在每個瀏覽器分別的記憶體:

10. 瀏覽器記憶體說明

當我們說「存在瀏覽器記憶體」,通常指的是 JavaScript 程式運行時 (runtime) 在 RAM 裡建立的物件。

例如你在 React 裡寫:const [user, setUser] = useState(null)

這個 user 實際上就是一個 JavaScript 物件,存在於瀏覽器的 JS 引擎(像 V8、SpiderMonkey)分配的記憶體空間,也就是電腦的 RAM。

特性:

所以「瀏覽器記憶體」≈「JS 執行時在 RAM 裡的暫存資料」,屬於揮發性記憶體。

業界實務上會不會用?

會,而且非常常見,但通常會搭配其他方式,避免刷新就掉光。

11. localStorage vs sessionStorage vs Cookie

特性 localStorage sessionStorage Cookie
存活時間 永久保存,除非使用者手動清除,或程式碼刪除 只在「分頁」存在,分頁關閉就消失 可以設置 Expires 或 Max-Age,到期自動刪除
範圍 同網域所有分頁共享 只限當前分頁(同一網站開新分頁也不會共享) 預設跟 domain 綁定,可以跨分頁、跨 session 使用
大小限制 一般瀏覽器 ~5MB 一般瀏覽器 ~5MB 每個 cookie ~4KB,數量有限制
存取方式 localStorage.getItem("key") sessionStorage.getItem("key") 會自動隨 HTTP Request 帶給伺服器
常見用途 保存登入狀態、使用者偏好設定 表單暫存資料、分頁臨時狀態 安全儲存 JWT,設定 HttpOnly 防止 XSS
安全性 完全暴露給 JS → 如果被 XSS 攻擊,token 就會被盜 完全暴露給 JS → 如果被 XSS 攻擊,token 就會被盜 若設 HttpOnly → JS 不能存取(防 XSS)

12. Redux Persist 與 XSS 風險

為什麼會有 XSS 風險?

安全做法:

  1. 使用 Cookie 存 JWT
  2. 如果想用 redux-persist
  3. 防 XSS

13. 兩種登入方案比較

方案 1:JWT 只存 ID → 個人頁再 call API 取完整資料

流程:

  1. 登入 → 後端回傳 JWT(只含 user ID)
  2. 前端把 JWT 存 cookie(建議 HttpOnly)
  3. 使用者進入個人頁 → 前端帶 JWT call /me 或 /profile API
  4. 後端驗證 JWT → 回傳完整使用者資料
  5. Redux / Context 只作 UI 快取

優點:

缺點:

方案 2:JWT + 個人資料一起存到 Redux Persist

流程:

  1. 登入 → 後端回傳 JWT + 個人資料
  2. 前端存進 Redux Persist + cookie
  3. 每次頁面載入,從 Redux Persist 讀取 user 資料,不一定再 call API

優點:

缺點:

建議做法(Hybrid):

  1. 登入後,call /me API 拿一次完整 user 資料 → 存在 Redux / Context(快取)
  2. 之後各頁面直接取 Redux / Context,不用每頁都 call API
  3. 重要資訊 / token 永遠存 HttpOnly Cookie → 防 XSS

14. 實務流程說明

情境 1:使用者從登入頁進入個人頁(正常流程)

  1. 使用者在登入頁輸入帳號密碼 → call /login
  2. 後端回傳 JWT,存 HttpOnly Cookie
  3. 前端登入成功後 → 導向個人頁
  4. 在個人頁初始化時 → call /me API
  5. 後端驗證 Cookie 裡的 JWT
  6. 回傳完整 user 資料
  7. 前端把 user 存進 Redux / Context(記憶體快取)
  8. 之後各個頁面都可以直接從 Redux / Context 拿資料,不用每頁再 call API

情境 2:使用者直接在個人頁輸入網址(或刷新頁面)

  1. 這時候 Redux / Context 還沒初始化(記憶體被刷新或新開分頁)
  2. 前端沒辦法直接拿到 user 資料
  3. 個人頁初始化 → 自動 call /me API
  4. 後端驗證 Cookie 裡的 JWT
  5. 回傳完整 user 資料 → 更新 Redux / Context

小結:

15. Redux 與分頁狀態

問題:如果我存 redux,我開新分頁,狀態會消失麼?

答案:對,會消失。

Redux 本質上就是存在瀏覽器的 JS 記憶體 (RAM)。所以:

實務解法(避免 Redux 狀態一刷新就掉)

如果你希望 Redux 狀態跨分頁/刷新也能保存,業界會搭配「持久化」方案:

  1. redux-persist
  2. 自己手動存/取 localStorage
  3. Cookie + 後端 API(更安全的做法)

結論:

16. React App 中的 user 存放位置

在 React 程式碼中:

const { user, loading: authLoading, signIn, signUp, signOut } = useAuth()

user 是從 useAuth() 這個自訂 hook 拿到的。要知道 user 存在哪裡,就要看 useAuth 的實作。

常見的 useAuth 實作方式:

  1. 存在 React state (記憶體)

    useAuth 可能裡面有 useState,例如:

    const [user, setUser] = useState<User | null>(null)

    當你登入 (signIn) 時,它會把後端回傳的使用者資訊(例如 id, email, name)存進 user 這個 state。

    特性:只存在瀏覽器記憶體。頁面一旦刷新 (F5) → state 會消失 → 通常要再去後端驗證 JWT 重新抓一次 user 資料。

  2. 存在 Context (全域狀態)

    很多專案會用 AuthContext 包起來:

    const AuthContext = createContext<AuthState>(...)

    然後 useAuth 只是 useContext(AuthContext) 的封裝。

    這樣一來,不同 component 都可以拿到同一份 user,不用一直傳 props。

  3. 存在 localStorage / sessionStorage

    有些 useAuth 會把登入後的 user 或 JWT 存到 localStorage,重新整理時可以從 localStorage 讀出來,避免掉線。

    useEffect(() => {
      const savedUser = localStorage.getItem("user")
      if (savedUser) setUser(JSON.parse(savedUser))
    }, [])
                

    這樣就算刷新頁面,user 仍然存在(直到使用者登出)。

  4. 存在 Cookie + 後端驗證

    如果 JWT 已經存在 cookie(特別是 HttpOnly cookie),那麼 useAuth 通常會在 component mount 時呼叫 /me API:

    useEffect(() => {
      fetch("/api/me", { credentials: "include" })
        .then(res => res.json())
        .then(data => setUser(data))
    }, [])
                

    好處:不用在前端存敏感資訊,安全性較高。

    缺點:需要額外一次 API 請求。

結論:

在程式碼裡的 user:

17. 總結