iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Modern Web

Golang x Echo 30 天:零基礎GO , 後端入門系列 第 24

以 Go + Echo 打造部落格|第 8 集:JWT 魔法小卡

  • 分享至 

  • xImage
  •  

前面幾集我們搞定了網站後台的 Session 登入,但如果你的服務需要讓手機 App 或其他「機器」來存取,Session 就不好用了。這時候,我們需要更酷、更現代的魔法小卡:JWT (JSON Web Token)!

📅 開發設定筆記

在開始之前,統一一下我們的基礎設定:

時區:Asia/Taipei(+08:00)

日期格式:2006-01-02

✏️ 這次改了什麼?(重點整理)

想像你正在組裝一台樂高機器人,這次我們加了幾個超重要的零件:

新增 🔐 登入關卡:api_auth.go (處理登入,發給你魔法小卡的地方)

新增 👑 管理者專區:api_admin.go (只有有卡的人才能進去)

調整 🛣️ 交通總部:main.go (設定「認卡」的檢查站)

新增 🗝️ 密碼鎖:.env.example (給 JWT 魔法小卡加一個超級難的密碼鎖 JWT_SECRET)

  1. 🔐 登入發卡機:internal/http/handlers/api_auth.go

這個檔案負責接收使用者的 E-mail 和密碼,進行驗證後,發放一張有期限的 JWT 魔法小卡 (access_token)。

我們設定卡片只有 15 分鐘 的壽命 (15 * time.Minute)。

package handlers

import (
	"net/http"
	"os"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/labstack/echo/v4"
	"golang.org/x/crypto/bcrypt"
)

type APIAuthHandler struct {
	Users UsersRepo // 從第 7 篇沿用:FindByEmail(ctx, email)
}

func NewAPIAuthHandler(users UsersRepo) *APIAuthHandler {
	return &APIAuthHandler{Users: users}
}

type tokenReq struct {
	Email    string `json:"email" form:"email"`
	Password string `json:"password" form:"password"`
}

type tokenResp struct {
	AccessToken string `json:"access_token"`
	ExpiresIn   int64  `json:"expires_in"`
}

func (h *APIAuthHandler) IssueToken(c echo.Context) error {
	var req tokenReq
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, echo.Map{"error": "bad request"})
	}
	// 驗證 E-mail 和密碼
	u, err := h.Users.FindByEmail(c, req.Email)
	if err != nil || bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(req.Password)) != nil {
		return c.JSON(http.StatusUnauthorized, echo.Map{"error": "invalid credentials"})
	}

	// 設定 15 分鐘過期
	exp := time.Now().Add(15 * time.Minute)
	claims := jwt.MapClaims{
		"uid":  u.ID,
		"role": u.Role,
		"exp":  exp.Unix(),
		"iat":  time.Now().Unix(),
		"iss":  "go-echo-blog", // 發行者
	}
	t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// 用 JWT_SECRET 簽名加密
	secret := []byte(os.Getenv("JWT_SECRET"))
	signed, err := t.SignedString(secret)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, echo.Map{"error": "sign token failed"})
	}

	return c.JSON(http.StatusOK, tokenResp{
		AccessToken: signed,
		ExpiresIn:   int64(time.Until(exp).Seconds()),
	})
}

  1. 👑 管理者專區:internal/http/handlers/api_admin.go

這個檔案是受保護的 API 邏輯。它會從 echo.Context 中取出經由 Middleware 驗證後解析出來的 JWT 資訊,並傳回使用者的 uid 和 role。

package handlers

import (
	"net/http"

	"github.com/golang-jwt/jwt/v5"
	"github.com/labstack/echo/v4"
)

type APIAdminHandler struct{}

func NewAPIAdminHandler() *APIAdminHandler { return &APIAdminHandler{} }

// GET /api/admin/secret
func (h *APIAdminHandler) Secret(c echo.Context) error {
	// 從 Context 取得 JWT 解析後的資訊
	user := c.Get("user").(*jwt.Token)
	claims := user.Claims.(jwt.MapClaims)

	return c.JSON(http.StatusOK, echo.Map{
		"message": "歡迎來到管理者 API",
		"uid":     claims["uid"],
		"role":    claims["role"],
	})
}

  1. 🛣️ 交通總部:cmd/server/main.go (路由與檢查站設定)

在這裡,我們設定了發卡路徑,並使用 middleware.JWTWithConfig 建立了一個 JWT 檢查站,將所有 /api/admin 開頭的路由都包在這個檢查站裡。

package main

import (
	"net/http"
	"os"

	"github.com/gorilla/sessions"
	"github.com/labstack/echo-contrib/session"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"

	"your/module/internal/http/handlers"
)

func main() {
	e := echo.New()
	e.Use(middleware.Recover())
	e.Use(middleware.Logger())
	e.Use(middleware.CORS()) // 開發先放寬,正式白名單

	// 後台 Session(沿用第 7 篇的設定)
	e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))))

	// ===== JWT 區 - 驗證與路由設定 =====
	usersRepo := buildUsersRepo() // 你的實作 (請確保你有實作這個函式)
	apiAuth := handlers.NewAPIAuthHandler(usersRepo)
	// 登入路徑:POST /api/auth/token 
	e.POST("/api/auth/token", apiAuth.IssueToken) 

	// 設置 JWT 檢查站設定
	jwtCfg := middleware.JWTConfig{
		SigningKey:  []byte(os.Getenv("JWT_SECRET")), // 簽名金鑰
		TokenLookup: "header:Authorization", // Token 會從 HTTP Header 的 Authorization 欄位取
		AuthScheme:  "Bearer", // 格式為 Authorization: Bearer <token>
	}
	
	// 設定 /api/admin 路由群組,並強制通過 JWT 檢查站
	apiAdmin := e.Group("/api/admin", middleware.JWTWithConfig(jwtCfg)) 

	admin := handlers.NewAPIAdminHandler()
	apiAdmin.GET("/secret", admin.Secret)

	// 健檢
	e.GET("/health", func(c echo.Context) error { return c.String(http.StatusOK, "ok") })
	e.GET("/_ping", func(c echo.Context) error { return c.String(http.StatusOK, "pong") })

	e.Logger.Fatal(e.Start(":1323"))
}

  1. 🗝️ 設定檔:.env.example

記得在你的 .env 檔案中加入這個秘密金鑰!請務必修改 please_change_me_now 成一串夠長、夠亂的密碼!

JWT_SECRET=please_change_me_now

✅ 驗收清單(DoD)

完成後,請用工具(如 Postman 或 cURL)測試,確保流程正確:

成功發卡:POST /api/auth/token 能拿到 access_token 與 expires_in。

拒絕訪問:不帶或帶錯誤的 Token 打 /api/admin/* → 收到 401 Unauthorized。

成功訪問:帶正確 Token 打 /api/admin/secret → 看到自己的 uid/role。

💡 小叮嚀(安全與維運)

雖然 JWT 方便,但有些細節要注意,才不會被駭客有機可乘:

Token 設短效:我們設了 15 分鐘,如果 Token 被偷了,損失也會被限制在這個時間內。

前端請藏好:前端(網頁/App)拿到 Token 後,請放在記憶體或安全容器中,避免存放在 LocalStorage,減少被 XSS 攻擊偷走的風險。

強制登出:如果需要「一鍵讓全部裝置登出」的功能,請研究 token_version 版本號法,在 Token 裡加入一個版本號,Middleware 驗證版本不一致就拒絕,這樣就能隨時讓舊卡失效。

結語:現在你的服務已經能同時以兩種模式運作:

後台用 Session (服務「人」)

API 用 JWT (服務「機器」)

兩條線跑起來,你的服務擴展性就大大提升了!


上一篇
以 Go + Echo 打造部落格|第 7 集:把後台上鎖啦!
系列文
Golang x Echo 30 天:零基礎GO , 後端入門24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言